CodeGym /Cursos /C# SELF /Exceções em Parallel.For

Exceções em Parallel.For e Parallel.ForEach

C# SELF
Nível 61 , Lição 2
Disponível

1. Como as exceções funcionam em Parallel.For e Parallel.ForEach

No loop for normal é simples: se um exception for lançado dentro do corpo do loop — a execução termina e o exception sobe pra fora. Nos loops paralelos não é assim. Vamos destrinchar.

Todas as exceções são juntadas em um único "saco"

Quando numa das iterações do loop paralelo (Parallel.For/ForEach) ocorre uma exceção, ela não é imediatamente lançada pra fora, ela é empacotada. O processo continua: outras iterações ou terminam normalmente, ou também lançam exceções. Resultado: quando o loop paralelo termina (ou é forçado a parar), todas as exceções "lançadas" são coletadas e lançadas como um único objeto do tipo AggregateException.

AggregateException é um "contêiner" que guarda uma coleção de todas as exceções que aconteceram durante as iterações paralelas. Isso é útil: a gente recebe sempre TODOS os erros (ou pelo menos todos os que deram pra acumular até o fim das threads ativas).

Como isso parece na prática

Exemplo: processamento paralelo onde às vezes lançamos uma exceção

using System;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 0, 4, 0, 6, 7, 8 };

        try
        {
            Parallel.ForEach(numbers, number =>
            {
                // Nós intencionalmente dividimos por esse número, às vezes ele é zero!
                // Isso vai causar DivideByZeroException
                int result = 100 / number;
                Console.WriteLine($"100 / {number} = {result}");
            });
        }
        catch (AggregateException ex)
        {
            Console.WriteLine("Erros detectados no loop paralelo!");

            // Iteramos por todas as exceções que aconteceram
            foreach (var inner in ex.InnerExceptions)
            {
                Console.WriteLine($"Tipo: {inner.GetType().Name} — Mensagem: {inner.Message}");
            }
        }
    }
}

O que vai acontecer:

  • No array principal tem zeros, e dividir por 0 é tabu na matemática (e no C#): vão ocorrer DivideByZeroException.
  • O loop paralelo começa a processar. Assim que em algum lugar ocorrer divisão por zero — o loop não para imediatamente, ele continua as iterações que já tinham começado.
  • Quando todas as threads terminarem (algumas com erro, outras sem), será lançado pra fora um AggregateException contendo todas as exceções que aconteceram.

Visualizando a mecânica do tratamento de exceções

flowchart LR
    A[Thread 1]
    B[Thread 2]
    C[Thread 3]
    D[Thread 4]
    E[Parallel.ForEach]
    F[Exceção 1]
    G[Exceção 2]
    H[AggregateException]
    subgraph Iterações
      A --> F
      B --> G
      C --> E
      D --> E
      F --> H
      G --> H
      E --> H
    end

No diagrama dá pra ver: threads diferentes podem encontrar erros diferentes, e no final todos eles são "empacotados" num único AggregateException.

2. Aspectos práticos do tratamento de erros

O que fazer com AggregateException?

Ao capturar AggregateException, geralmente há dois cenários:

  • Exibir pro usuário (ou pro log) todas as falhas, pra aprender com elas.
  • Entender qual erro é crítico e quais são bobagens: decidir se considerar toda a operação como falha ou ignorar alguns erros isolados.

Padrão comum: tratar via Handle

try
{
    Parallel.For(0, 10, i =>
    {
        if (i == 3 || i == 7)
            throw new InvalidOperationException($"Erro na iteração {i}");
        Console.WriteLine($"Processado: {i}");
    });
}
catch (AggregateException ex)
{
    ex.Handle(e =>
    {
        if (e is InvalidOperationException)
        {
            Console.WriteLine("Erro pego: " + e.Message);
            // true = erro considerado tratado
            return true;
        }
        // false = não tratado, será relançado
        return false;
    });
}

Esse approach permite tratar só os erros que você considera "normais", e deixar o resto subir de novo, pra não mascarar falhas críticas.

Nuances interessantes (e perigosas) da implementação

Quando o loop para?
Quando uma iteração lança uma exceção, Parallel.For/ForEach não inicia novas iterações, mas as que já começaram continuam executando. Depois que todas as iterações ativas terminarem, é lançado o AggregateException. Se há muitas threads ativas, o "rabo" do trabalho ainda vai terminar — por isso pode haver várias exceções.

Se você não capturar a exceção, a aplicação vai cair.
Se você não envolver Parallel.For/ForEach num bloco try-catch, a aplicação vai encerrar de forma anormal na primeira falha detectada após o término de todas as iterações — não é legal com o usuário.

Empurrando o tratamento pra dentro do loop.
Às vezes você quer que iterações individuais não estraguem tudo; aí trata as exceções diretamente dentro do corpo do loop paralelo:

Parallel.ForEach(numbers, number =>
{
    try
    {
        int result = 100 / number;
        Console.WriteLine(result);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Erro no número {number}: {ex.Message}");
    }
});

Esse jeito é bom quando você não precisa de todas as exceções "aggregadas" — você já lida com cada falha na hora (por exemplo, log). Mas cuidado: se fizer isso, nenhuma AggregateException vai aparecer e você não terá uma visão global se tudo deu certo.

Se Break() ou Stop() for chamado.
Se uma iteração chamar ParallelLoopState.Break() ou ParallelLoopState.Stop(), o loop tenta impedir novas iterações: Break() termina iterações após o índice corrente, enquanto Stop() tenta parar todas. Contudo, se uma exceção ocorrer ao mesmo tempo, ela será armazenada e lançada como AggregateException depois que todas as iterações ativas terminarem.

3. Dicas úteis

Exceções em loops normais vs paralelos

No loop normal qualquer erro faz o trabalho parar imediatamente: a exceção sobe pra fora e tudo pára.

Nos loops paralelos o C# segue uma abordagem mais pragmática: o trabalho continua nas tasks já iniciadas, e só no fim todas as falhas "saem" de uma vez. Isso permite coletar todos os erros sem perder nenhum e decidir o que fazer depois que o loop terminar.

4. Erros comuns ao trabalhar com exceções em Parallel.For e Parallel.ForEach

Erro #1: ignorar o AggregateException.
Se não capturar AggregateException, a aplicação vai cair depois que todas as iterações terminarem, causando perda de dados e falhas em apps server-side ou GUI.

Erro #2: usar .Wait() sem try-catch.
Chamar .Wait() para Parallel.For/ForEach sem tratar AggregateException vai resultar em exceção não tratada, dificultando diagnóstico.

Erro #3: ignorar erros repetidos.
Múltiplos erros idênticos (por exemplo, divide by zero) podem vir de dados repetidos. Sem analisar InnerExceptions você pode perder a causa raiz.

Erro #4: silenciar todas as exceções.
Usar catch (Exception) { /* vazio */ } dentro do loop esconde problemas, levando a perda de informação e bugs "fantasmas".

Comportamento de erros em diferentes tipos de loop

Opção For/foreach normal Parallel.For / ForEach
Exceção é repassada Imediatamente Depois do término de todas as iterações
Formato do erro Exception único AggregateException com coleção
Outras iterações Não são executadas As que já começaram terminam
Capturar erro no corpo Sim Sim
Capturar erro "por fora" Sim Sim, via AggregateException
  • O que acontece se você não tratar AggregateException?
    A aplicação vai cair depois do término de todas as iterações — independente de onde/quando o erro ocorreu.
  • Dá pra saber em qual iteração a exceção ocorreu?
    Só se você mesmo incluir no exception info o índice ou os dados.
  • AggregateException pode estar vazio?
    Não, ele é criado apenas se houver pelo menos uma exceção interna. Se não houver erros, ele não é lançado.
  • Erros tratados dentro do loop são considerados?
    Sim, mas aí nada sobe pra fora e AggregateException não vai existir.

Agora você está pronto não só pra rodar loops multi-thread, mas também pra lidar com as "batidas" paralelas com jeitinho! E, como sempre, tome cuidado com concorrência: ela adora surpresas, especialmente quando ninguém está pronto pra pegá-las.

Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION