1. Wie Ausnahmen in Parallel.For und Parallel.ForEach funktionieren
In einer normalen for-Schleife ist alles einfach: wenn im Schleifenrumpf eine Ausnahme geworfen wird — wird die Schleife beendet und die Ausnahme fliegt nach außen. In parallelen Schleifen ist das anders. Lass uns das genauer anschauen.
Alle Ausnahmen werden in eine "Tüte" gesammelt
Wenn in einer der Iterationen der parallelen Schleife (Parallel.For/ForEach) eine Ausnahme auftritt, wird sie nicht sofort nach außen geworfen, sondern verpackt. Der Prozess geht weiter: andere Iterationen laufen entweder zu Ende oder werfen ebenfalls Ausnahmen. Ergebnis: wenn die parallele Schleife ihre Ausführung beendet (oder zwangsweise abgebrochen wird), werden alle "geworfenen" Ausnahmen gesammelt und als ein Objekt vom Typ AggregateException nach außen geworfen.
AggregateException ist ein "Container", der intern eine Sammlung aller Ausnahmen enthält, die während der Ausführung der parallelen Iterationen aufgetreten sind. Das ist praktisch: wir bekommen IMMER ALLE Fehler (oder zumindest alle, die sich bis zum Abschluss der Hauptthreads angesammelt haben).
Wie das in der Praxis aussieht
Beispiel: parallele Verarbeitung, bei der manchmal eine Ausnahme geworfen wird
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 =>
{
// Wir teilen absichtlich durch die Zahl, manchmal ist sie null!
// Das verursacht eine DivideByZeroException
int result = 100 / number;
Console.WriteLine($"100 / {number} = {result}");
});
}
catch (AggregateException ex)
{
Console.WriteLine("Fehler in der parallelen Schleife entdeckt!");
// Wir iterieren über alle aufgetretenen Ausnahmen
foreach (var inner in ex.InnerExceptions)
{
Console.WriteLine($"Typ: {inner.GetType().Name} — Nachricht: {inner.Message}");
}
}
}
}
Was passieren wird:
- In der Hauptkollektion sind Nullen, und Division durch 0 ist in der Mathematik (und in C#) tabu: es entstehen DivideByZeroException.
- Die parallele Schleife startet die Verarbeitung. Sobald irgendwo eine Division durch Null passiert — stoppt die Schleife nicht sofort, sondern führt die Iterationen weiter aus, die bereits begonnen wurden.
- Wenn alle Threads ihre Arbeit beendet haben (einige mit Fehlern, einige ohne), wird nach außen eine AggregateException geworfen, die alle aufgetretenen Ausnahmen enthält.
Visualisierung der Mechanik der Ausnahmebehandlung
flowchart LR
A[Thread 1]
B[Thread 2]
C[Thread 3]
D[Thread 4]
E[Parallel.ForEach]
F[Ausnahme 1]
G[Ausnahme 2]
H[AggregateException]
subgraph Iterationen
A --> F
B --> G
C --> E
D --> E
F --> H
G --> H
E --> H
end
Auf dem Diagramm sieht man: verschiedene Threads können auf verschiedene Fehler stoßen, und am Ende werden alle in einer einzigen AggregateException "verpackt".
2. Praktische Besonderheiten der Fehlerbehandlung
Was macht man mit AggregateException?
Wenn man eine AggregateException fängt, gibt es in der Regel zwei Szenarien:
- Alle Fehler dem Benutzer (oder dem Log) ausgeben, um daraus zu lernen.
- Verstehen, welcher Fehler kritisch ist und welche Nebensächlichkeiten sind: entscheiden, ob die ganze Operation als fehlgeschlagen gilt oder einzelne Fehler ignoriert werden können.
Typisches Muster: Behandlung via Handle
try
{
Parallel.For(0, 10, i =>
{
if (i == 3 || i == 7)
throw new InvalidOperationException($"Fehler in Iteration {i}");
Console.WriteLine($"Verarbeitet: {i}");
});
}
catch (AggregateException ex)
{
ex.Handle(e =>
{
if (e is InvalidOperationException)
{
Console.WriteLine("Gefangener Fehler: " + e.Message);
// true = die Ausnahme gilt als behandelt
return true;
}
// false = nicht behandelt, wird erneut geworfen
return false;
});
}
Dieser Ansatz ermöglicht es, nur die Fehler zu behandeln, die du als "normal" ansiehst, und alles andere nach oben durchzureichen, damit kritische Ausfälle nicht übersehen werden.
Interessante (und gefährliche) Implementierungsnuancen
Wann stoppt die Schleife?
Wenn in einer Iteration eine Ausnahme auftritt, startet Parallel.For/ForEach keine neuen Iterationen mehr, aber bereits gestartete Iterationen laufen weiter. Nach Abschluss aller aktiven Iterationen wird eine AggregateException geworfen. Wenn es viele Threads gibt, wird der "Schwanz" der Arbeit trotzdem zu Ende gebracht — deshalb kann es mehrere Fehler geben.
Wenn man die Ausnahme nicht fängt, stürzt die Anwendung ab.
Wenn man Parallel.For/ForEach nicht mit einem try-catch umgibt, wird die Anwendung nach dem Abschluss aller Iterationen beim ersten auftretenden Fehler abstürzen — nicht sehr freundlich gegenüber dem Benutzer.
Ausnahme "innerhalb" der Schleife behandeln.
Manchmal braucht man einen speziellen Ansatz. Zum Beispiel, wenn du nicht willst, dass einzelne Iterationen das Gesamtbild verderben, kannst du Ausnahmen direkt im Körper der parallelen Schleife behandeln:
Parallel.ForEach(numbers, number =>
{
try
{
int result = 100 / number;
Console.WriteLine(result);
}
catch (Exception ex)
{
Console.WriteLine($"Fehler bei Zahl {number}: {ex.Message}");
}
});
Diese Methode ist gut, wenn du nicht alle Ausnahmen "auf einen Schlag" brauchst — du behandelst jeden Fehler sofort vor Ort (z. B. schreibst ihn ins Log). Aber Vorsicht: wenn du so machst, entsteht keine AggregateException und du kannst nicht mehr leicht feststellen, ob insgesamt alles in Ordnung war.
Wenn Break() oder Stop() aufgerufen werden.
Wenn eine Iteration ParallelLoopState.Break() oder ParallelLoopState.Stop() aufruft, versucht die Schleife, neue Iterationen zu stoppen: Break() beendet Iterationen nach dem aktuellen Index, und Stop() — alle Iterationen. Falls gleichzeitig eine Ausnahme auftritt, wird diese behalten und nach Abschluss aller aktiven Iterationen als AggregateException geworfen.
3. Nützliche Feinheiten
Ausnahmen in normalen Schleifen vs. parallelen Schleifen
In einer normalen Schleife führt jeder Fehler zu einem sofortigen Abbruch der gesamten Arbeit: die Ausnahme wird nach außen geworfen, alles blockiert.
In parallelen Schleifen verfolgt C# einen kompromissfreudigeren Ansatz: die Arbeit geht für bereits gestartete Tasks weiter, und erst nach Abschluss des gesamten Prozesses werden alle Fehler gesammelt und zusammen nach außen gegeben. So lassen sich alle Fehler sammeln, ohne einen einzigen zu verlieren, und man kann nach Abschluss der Schleife entscheiden, wie weiter vorzugehen ist.
4. Typische Fehler beim Arbeiten mit Ausnahmen in Parallel.For und Parallel.ForEach
Fehler #1: Ignorieren der AggregateException.
Wenn man die AggregateException nicht fängt, stürzt die Anwendung nach Abschluss aller Iterationen ab, was zu Datenverlust und Problemen in Server- oder GUI-Anwendungen führen kann.
Fehler #2: .Wait() verwenden ohne try-catch.
Der Aufruf von .Wait() für Parallel.For/ForEach ohne Behandlung der AggregateException führt zu einer unhandled exception, was die Diagnose erschwert.
Fehler #3: Ignorieren sich wiederholender Fehler.
Mehrere identische Fehler (z. B. Division durch Null) können durch wiederholte Daten auftreten. Ohne Analyse von InnerExceptions kann die eigentliche Ursache unentdeckt bleiben.
Fehler #4: Alle Ausnahmen zum Schweigen bringen.
Ein catch (Exception) { /* leer */ } innerhalb der Schleife verschleiert Fehler, was zu Informationsverlust und "Geister"-Bugs führt.
Fehlerverhalten in verschiedenen Schleifen
| Variante | Normale for/foreach | Parallel.For / ForEach |
|---|---|---|
| Wann die Ausnahme behandelt wird | Sofort | Nach Abschluss aller Iterationen |
| Fehlerformat | Einzelne exception | AggregateException mit Collection |
| Andere Iterationen | Werden nicht mehr ausgeführt | Bereits gestartete werden zu Ende geführt |
| Fehler im Körper fangen | Ja | Ja |
| Fehler "von außen" fangen | Ja | Ja, via AggregateException |
"Gimmicks" und kurze Interviewfragen:
- Was passiert, wenn man die AggregateException nicht behandelt?
Die Anwendung stürzt nach Abschluss aller Iterationen ab — unabhängig davon, wo und wann der Fehler aufgetreten ist. - Kann man herausfinden, in welcher Iteration genau ein Fehler entstanden ist?
Nur, wenn du selbst Informationen über den Index oder die Daten in die Exception einbaust. - Kann eine AggregateException leer sein?
Nein, sie wird nur erstellt, wenn mindestens eine innere Ausnahme vorhanden ist. Wenn keine Fehler auftreten, wird sie nicht geworfen. - Werden Fehler behandelt, wenn man sie im Körper fängt?
Ja, aber dann wird außen nichts mehr fliegen und es entsteht keine AggregateException.
Jetzt seid ihr bereit, Schleifen über mehrere Threads zu starten und gleichzeitig deren parallele "Unfälle" geschickt zu managen! Und wie immer — seid vorsichtig mit Multithreading: es liebt Überraschungen, besonders wenn niemand sie fängt.
GO TO FULL VERSION