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.
A aplicação vai cair depois do término de todas as iterações — independente de onde/quando o erro ocorreu.
Só se você mesmo incluir no exception info o índice ou os dados.
Não, ele é criado apenas se houver pelo menos uma exceção interna. Se não houver erros, ele não é lançado.
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.
GO TO FULL VERSION