CodeGym /Kursy /C# SELF /Wyjątki w Parallel.For

Wyjątki w Parallel.For i Parallel.ForEach

C# SELF
Poziom 61 , Lekcja 2
Dostępny

1. Działanie wyjątków w Parallel.For i Parallel.ForEach

W zwykłej pętli for wszystko jest proste: jeśli w ciele pętli zostanie rzucony wyjątek — wykonanie pętli się kończy i wyjątek poleci na zewnątrz. W pętlach równoległych jest inaczej. Rozkmini my to.

Wszystkie wyjątki są zbierane do jednego "worka"

Kiedy w jednej z iteracji pętli równoległej (Parallel.For/ForEach) pojawia się wyjątek, nie leci on od razu na zewnątrz, tylko jest pakowany. Proces trwa dalej: inne iteracje albo dokańczają, albo też rzucają wyjątki. Efekt: gdy pętla równoległa kończy wykonanie (albo jest wymuszanie przerwana), wszystkie "rzucone" wyjątki są zbierane i wyrzucane na zewnątrz jako jeden obiekt typu AggregateException.

AggregateException — to "kontener", który trzyma wewnątrz kolekcję wszystkich wyjątków, które wystąpiły podczas wykonywania równoległych iteracji. To wygodne: zawsze dostajemy WSZYSTKIE błędy (albo przynajmniej te, które zdążyły się zebrać przed zakończeniem głównych wątków).

Jak to wygląda w praktyce

Przykład: przetwarzanie równoległe, gdzie czasem rzucamy wyjątek

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 =>
            {
                // Celowo dzielimy przez liczbę, czasem jest równa zero!
                // To spowoduje DivideByZeroException
                int result = 100 / number;
                Console.WriteLine($"100 / {number} = {result}");
            });
        }
        catch (AggregateException ex)
        {
            Console.WriteLine("Wykryto błędy w pętli równoległej!");

            // Iterujemy po wszystkich wyjątkach, które się zdarzyły
            foreach (var inner in ex.InnerExceptions)
            {
                Console.WriteLine($"Typ: {inner.GetType().Name} — Komunikat: {inner.Message}");
            }
        }
    }
}

Co się stanie:

  • W głównej kolekcji są zera, a dzielenie przez 0 — to tabu w matematyce (i w C#): wystąpią DivideByZeroException.
  • Pętla równoległa zaczyna przetwarzanie. Jak tylko gdzieś wystąpi dzielenie przez zero — pętla nie zatrzyma się od razu, a dokończy wszystkie iteracje, które już się zaczęły wykonywać.
  • Kiedy wszystkie wątki zakończą pracę (kto z błędem, kto bez), na zewnątrz poleci AggregateException, zawierający wszystkie wystąpienia wyjątków.

Zobrazujmy mechanikę obsługi wyjątków

flowchart LR
    A[Wątek 1]
    B[Wątek 2]
    C[Wątek 3]
    D[Wątek 4]
    E[Parallel.ForEach]
    F[Wyjątek 1]
    G[Wyjątek 2]
    H[AggregateException]
    subgraph Iteracje
      A --> F
      B --> G
      C --> E
      D --> E
      F --> H
      G --> H
      E --> H
    end

Na schemacie widać: różne wątki mogą natrafić na różne błędy i w końcu wszystkie są "pakowane" w jeden AggregateException.

2. Praktyczne cechy obsługi błędów

Co zrobić z AggregateException?

Kiedy łapiemy AggregateException, zwykle są dwa scenariusze:

  • Wyświetlić użytkownikowi (albo do loga) wszystkie błędy, żeby się uczyć na błędach.
  • Zrozumieć, który błąd jest krytyczny, a które to drobnostki: zdecydować, czy całą operację traktować jako nieudaną, czy zignorować pojedyncze awarie.

Typowy wzorzec: obsługa przez Handle

try
{
    Parallel.For(0, 10, i =>
    {
        if (i == 3 || i == 7)
            throw new InvalidOperationException($"Błąd w iteracji {i}");
        Console.WriteLine($"Przetworzono: {i}");
    });
}
catch (AggregateException ex)
{
    ex.Handle(e =>
    {
        if (e is InvalidOperationException)
        {
            Console.WriteLine("Złapano błąd: " + e.Message);
            // true = błąd uznany za obsłużony
            return true;
        }
        // false = nieobsłużone, zostanie rzucone ponownie
        return false;
    });
}

Takie podejście pozwala obsłużyć tylko te błędy, które uważasz za "normalne", a resztę — przepuścić wyżej, żeby nie przegapić krytycznych awarii.

Ciekawe (i niebezpieczne) niuanse implementacji

Kiedy pętla się zatrzymuje?
Kiedy w iteracji pojawia się wyjątek, Parallel.For/ForEach nie uruchamia nowych iteracji, ale już rozpoczęte kontynuują wykonywanie. Po zakończeniu wszystkich aktywnych iteracji wyrzucany jest AggregateException. Jeśli wątków jest dużo, "ogon" pracy i tak dobiegnie końca — dlatego błędów może być kilka.

Jeśli nie złapiesz wyjątku, aplikacja się rozleci.
Jeśli nie owiniesz Parallel.For/ForEach w blok try-catch, aplikacja zakończy się awaryjnie przy pierwszym napotkanym błędzie po zakończeniu wszystkich iteracji — niezbyt grzeczne wobec użytkownika.

Przepuszczenie wyjątku "do środka" pętli.
Czasem potrzebne jest specjalne podejście. Na przykład, jeśli chcesz, żeby pojedyncze iteracje nie psuły całego obrazu, możesz obsłużyć wyjątki bezpośrednio w ciele pętli równoległej:

Parallel.ForEach(numbers, number =>
{
    try
    {
        int result = 100 / number;
        Console.WriteLine(result);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Błąd dla liczby {number}: {ex.Message}");
    }
});

Ten sposób jest dobry, jeśli nie potrzebujesz wszystkich wyjątków "hurtowo" — od razu obsługujesz każdą porażkę na miejscu (np. logujesz). Ale uwaga: jeśli tak robisz — żadne AggregateException nie powstanie i nie będziesz mógł sprawdzić, czy wszystko poszło OK globalnie.

Jeśli wywołane zostaną Break() lub Stop().
Jeśli iteracja wywoła ParallelLoopState.Break() lub ParallelLoopState.Stop(), pętla próbuje zatrzymać nowe iteracje: Break() kończy iteracje po bieżącym indeksie, a Stop() — wszystkie iteracje. Jednak jeśli równocześnie wystąpi wyjątek, to zostanie on zachowany i wyrzucony jako AggregateException po zakończeniu wszystkich aktywnych iteracji.

3. Przydatne niuanse

Wyjątki w zwykłych pętlach vs równoległych

W zwykłej pętli każdy błąd prowadzi do natychmiastowego zakończenia całej pracy: wyjątek idzie na zewnątrz, wszystko się blokuje.

W pętlach równoległych C# stosuje bardziej kompromisowe podejście: praca trwa dalej dla już wystartowanych zadań, i dopiero po zakończeniu całego procesu wszystkie błędy "wychodzą" na zewnątrz jedną porcją. To pozwala zebrać wszystkie błędy, nie tracąc żadnego, i podjąć decyzję po zakończeniu pętli.

4. Typowe błędy przy pracy z wyjątkami w Parallel.For i Parallel.ForEach

Błąd nr 1: ignorowanie AggregateException.
Jeśli nie złapiesz AggregateException, aplikacja zakończy się awaryjnie po zakończeniu wszystkich iteracji, co doprowadzi do utraty danych i awarii w aplikacjach serwerowych lub GUI.

Błąd nr 2: używanie .Wait() bez try-catch.
Wywołanie .Wait() dla Parallel.For/ForEach bez obsługi AggregateException doprowadzi do nieobsłużonego wyjątku, co utrudni diagnostykę.

Błąd nr 3: ignorowanie powtarzających się błędów.
Wielokrotne identyczne błędy (np. dzielenie przez zero) mogą być rzucone z powodu powtarzających się danych. Bez analizy InnerExceptions można przeoczyć sedno problemu.

Błąd nr 4: tłumienie wszystkich wyjątków.
Używanie catch (Exception) { /* pusto */ } wewnątrz pętli ukrywa błędy, co prowadzi do utraty ważnej informacji i "duchowych" bugów.

Zachowanie błędów w różnych pętlach

Wariant Zwykły for/foreach Parallel.For / ForEach
Wyjątek jest obsłużony Natychmiast Po zakończeniu wszystkich iteracji
Format błędu Pojedynczy exception AggregateException z kolekcją
Pozostałe iteracje Nie wykonują się Już uruchomione dokańczają
Łapanie błędów w ciele Tak Tak
Łapanie błędów "z zewnątrz" Tak Tak, przez AggregateException

"Sztuczki" i krótkie pytania na rozmowę:

  • Co się stanie, jeśli nie obsłużyć AggregateException?
    Aplikacja padnie po zakończeniu wszystkich iteracji — niezależnie od tego, gdzie i kiedy wystąpił błąd.
  • Czy można dowiedzieć się, w której iteracji wystąpił błąd?
    Tylko jeśli sam dodasz do wyjątku informację o indeksie albo danych.
  • Czy AggregateException może być pusty?
    Nie, tworzy się tylko w obecności przynajmniej jednego wewnętrznego wyjątku. Jeśli błędów nie ma, nie jest rzucany.
  • Czy błędy są obsługiwane, jeśli je złapiemy wewnątrz pętli?
    Tak, ale wtedy "na zewnątrz" nic nie poleci i AggregateException nie powstanie.

Teraz jesteś gotowy nie tylko odpalać pętle na wielu wątkach, ale też sprawnie radzić sobie z ich równoległymi "awariami"! I jak zwykle — uważaj na wielowątkowość: lubi niespodzianki, zwłaszcza jeśli nikt ich nie łapie.

2
Zadanie
C# SELF, poziom 61, lekcja 2
Niedostępne
Przykład prostego użycia Parallel.For z wyjątkiem
Przykład prostego użycia Parallel.For z wyjątkiem
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION