CodeGym /Corsi /C# SELF /Eccezioni in Parallel.For

Eccezioni in Parallel.For e Parallel.ForEach

C# SELF
Livello 61 , Lezione 2
Disponibile

1. Funzionamento delle eccezioni in Parallel.For e Parallel.ForEach

Nel normale ciclo for è semplice: se dentro il corpo del ciclo viene lanciata un'eccezione — l'esecuzione del ciclo termina e l'eccezione vola verso l'esterno. Nei cicli paralleli non è così. Vediamo perché.

Tutte le eccezioni vengono raccolte in un unico "sacco"

Quando in una delle iterazioni di un ciclo parallelo (Parallel.For/ForEach) si verifica un'eccezione, essa non viene immediatamente propagata fuori, ma viene impacchettata. Il processo continua: altre iterazioni o finiscono il loro lavoro, o a loro volta lanciano eccezioni. Risultato: quando il ciclo parallelo termina l'esecuzione (o viene interrotto forzatamente), tutte le eccezioni "lanciate" vengono raccolte e rilanciate fuori come un unico oggetto di tipo AggregateException.

AggregateException è un "contenitore" che dentro tiene la collezione di tutte le eccezioni avvenute durante l'esecuzione delle iterazioni parallele. È comodo: riceviamo sempre TUTTI gli errori (o almeno tutti quelli che si sono accumulati prima della fine dei thread principali).

Come appare in pratica

Esempio: elaborazione parallela che a volte lancia un'eccezione

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 =>
            {
                // Noi intenzionalmente dividiamo per il numero, a volte è zero!
                // Questo causerà una DivideByZeroException
                int result = 100 / number;
                Console.WriteLine($"100 / {number} = {result}");
            });
        }
        catch (AggregateException ex)
        {
            Console.WriteLine("Rilevati errori nel ciclo parallelo!");

            // Iteriamo su tutte le eccezioni che sono successe
            foreach (var inner in ex.InnerExceptions)
            {
                Console.WriteLine($"Tipo: {inner.GetType().Name} — Messaggio: {inner.Message}");
            }
        }
    }
}

Cosa succederà:

  • La collezione principale contiene zeri, e dividere per 0 è tabù in matematica (e in C#): si genereranno DivideByZeroException.
  • Il ciclo parallelo inizia l'elaborazione. Appena da qualche parte avviene una divisione per zero — il ciclo non si fermerà subito, ma continuerà tutte le iterazioni che hanno già iniziato l'esecuzione.
  • Quando tutti i thread avranno finito il lavoro (chi con errore, chi senza), verso l'esterno verrà lanciata una AggregateException contenente tutte le eccezioni verificatesi.

Visualizziamo la meccanica della gestione delle eccezioni

flowchart LR
    A[Thread 1]
    B[Thread 2]
    C[Thread 3]
    D[Thread 4]
    E[Parallel.ForEach]
    F[Eccezione 1]
    G[Eccezione 2]
    H[AggregateException]
    subgraph Iterazioni
      A --> F
      B --> G
      C --> E
      D --> E
      F --> H
      G --> H
      E --> H
    end

Nello schema si vede: thread diversi possono incontrare errori diversi, e tutti alla fine vengono "impacchettati" in un unico AggregateException.

2. Aspetti pratici della gestione degli errori

Cosa fare con AggregateException?

Quando si cattura AggregateException, di solito ci sono due scenari:

  • Mostrare all'utente (o nel log) tutti gli errori, per imparare dall'esperienza.
  • Capire quale errore è critico e quali sono trascurabili: decidere se considerare l'intera operazione fallita o ignorare singoli fallimenti.

Pattern tipico: gestione tramite Handle

try
{
    Parallel.For(0, 10, i =>
    {
        if (i == 3 || i == 7)
            throw new InvalidOperationException($"Errore nell'iterazione {i}");
        Console.WriteLine($"Elaborato: {i}");
    });
}
catch (AggregateException ex)
{
    ex.Handle(e =>
    {
        if (e is InvalidOperationException)
        {
            Console.WriteLine("Errore catturato: " + e.Message);
            // true = l'errore è considerato gestito
            return true;
        }
        // false = non gestito, verrà rilanciato
        return false;
    });
}

Questo approccio permette di gestire solo quegli errori che ritenete "normali", e tutto il resto — farlo risalire verso l'alto, per non perdere i guasti critici.

Sfaccettature interessanti (e pericolose) dell'implementazione

Quando il ciclo si ferma?
Quando in un'iterazione si verifica un'eccezione, Parallel.For/ForEach non avvia nuove iterazioni, ma quelle già avviate continuano ad eseguire. Dopo il termine di tutte le iterazioni attive viene lanciata una AggregateException. Se ci sono molti thread, la "coda" di lavoro arriverà comunque al termine — perciò gli errori possono essere multipli.

Se non si cattura l'eccezione, l'applicazione crasha.
Se non incapsulate Parallel.For/ForEach in un blocco try-catch, l'applicazione terminerà in modo anomalo alla prima eccezione incontrata dopo la fine di tutte le iterazioni — non molto gentile verso l'utente.

Propagare l'eccezione "dentro" il ciclo.
A volte serve un approccio particolare. Per esempio, se volete che singole iterazioni non rovinino il quadro generale, potete gestire le eccezioni direttamente nel corpo del ciclo parallelo:

Parallel.ForEach(numbers, number =>
{
    try
    {
        int result = 100 / number;
        Console.WriteLine(result);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Errore sul numero {number}: {ex.Message}");
    }
});

Questo metodo è utile se non vi servono tutte le eccezioni "in blocco" — gestite subito ogni fallimento localmente (per esempio, scrivendo nel log). Ma attenzione: così non si verificherà alcuna AggregateException e non potrete sapere se tutto è andato bene globalmente.

Se viene chiamato Break() o Stop().
Se un'iterazione chiama ParallelLoopState.Break() o ParallelLoopState.Stop(), il ciclo cerca di fermare le nuove iterazioni: Break() termina le iterazioni dopo l'indice corrente, mentre Stop() — tutte le iterazioni. Tuttavia, se contemporaneamente si verifica un'eccezione, questa viene salvata e rilanciata come AggregateException dopo la fine di tutte le iterazioni attive.

3. Note utili

Eccezioni nei cicli normali vs paralleli

Nel ciclo normale qualsiasi errore porta all'immediata conclusione del lavoro: l'eccezione vola fuori e tutto si blocca.

Nei cicli paralleli C# adotta un approccio più bilanciato: il lavoro continua per i task già avviati, e solo al termine dell'intero processo tutti gli errori "emergono" in blocco. Questo permette di raccogliere tutte le eccezioni senza perderne nessuna e decidere cosa fare dopo la fine del ciclo.

4. Errori tipici quando si lavora con le eccezioni in Parallel.For e Parallel.ForEach

Errore №1: ignorare AggregateException.
Se non catturate AggregateException, l'applicazione terminerà in modo anomalo dopo la fine di tutte le iterazioni, causando perdita di dati e malfunzionamenti in applicazioni server o GUI.

Errore №2: usare .Wait() senza try-catch.
Chiamare .Wait() per Parallel.For/ForEach senza gestire AggregateException porterà a un'eccezione non gestita, complicando la diagnosi.

Errore №3: ignorare errori ripetuti.
Molteplici errori identici (per esempio, divisione per zero) possono essere causati da dati ripetuti. Senza analizzare InnerExceptions si può perdere la causa principale.

Errore №4: soffocare tutte le eccezioni.
Usare catch (Exception) { /* vuoto */ } all'interno del ciclo nasconde gli errori, causando perdita di informazioni importanti e bug "fantasma".

Comportamento degli errori nei diversi cicli

Opzione Ciclo for/foreach normale Parallel.For / ForEach
L'eccezione viene gestita Immediatamente Dopo la fine di tutte le iterazioni
Formato dell'errore Singolo exception AggregateException con collezione
Altre iterazioni Non vengono eseguite Quelle già avviate finiscono
Cattura degli errori nel corpo
Cattura degli errori "dall'esterno" Sì, tramite AggregateException

"Trucchi" e domandine rapide per colloquio:

  • Cosa succede se non si gestisce AggregateException?
    L'applicazione crasha dopo la fine di tutte le iterazioni — indipendentemente da dove e quando è avvenuto l'errore.
  • È possibile sapere in quale iterazione è avvenuto l'errore?
    Solo se voi stessi includete nell'eccezione l'informazione sull'indice o sui dati.
  • Può AggregateException essere vuoto?
    No, viene creato solo se esiste almeno un'eccezione interna. Se non ci sono errori, non viene lanciato.
  • Gli errori vengono gestiti se li catturiamo dentro il ciclo?
    Sì, ma in quel caso "all'esterno" non uscirà nulla e non si genererà AggregateException.

Ora siete pronti non solo a lanciare cicli su più thread, ma anche a gestire con destrezza i loro "incidenti" paralleli! E, come sempre, — fate attenzione al multithreading: ama le sorprese, soprattutto se nessuno le cattura.

2
Compito
C# SELF, livello 61, lezione 2
Bloccato
Esempio di semplice utilizzo di Parallel.For con eccezione
Esempio di semplice utilizzo di Parallel.For con eccezione
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION