CodeGym /Cours /C# SELF /Exceptions dans Parallel.Fo...

Exceptions dans Parallel.For et Parallel.ForEach

C# SELF
Niveau 61 , Leçon 2
Disponible

1. Fonctionnement des exceptions dans Parallel.For et Parallel.ForEach

Dans une boucle for classique c'est simple : si une exception est lancée dans le corps de la boucle — l'exécution s'arrête et l'exception remonte. Dans les boucles parallèles c'est différent. On va décortiquer.

Toutes les exceptions sont rassemblées dans un seul "sac"

Quand une itération d'une boucle parallèle (Parallel.For/ForEach) lève une exception, elle n'est pas immédiatement propagée vers l'extérieur, elle est empaquetée. Le processus continue : d'autres itérations peuvent terminer ou lever aussi des exceptions. Au final : quand la boucle parallèle termine (ou est interrompue), toutes les exceptions "jetées" sont collectées et renvoyées sous la forme d'un seul objet de type AggregateException.

AggregateException est un "conteneur" qui contient une collection de toutes les exceptions survenues pendant l'exécution des itérations parallèles. Pratique : on obtient TOUS les erreurs (ou du moins toutes celles accumulées jusqu'à la fin des threads principaux).

À quoi ça ressemble en pratique

Exemple : traitement parallèle où on lance parfois une exception

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 =>
            {
                // Nous divisons volontairement par ce nombre, parfois il vaut zéro !
                // Cela va provoquer DivideByZeroException
                int result = 100 / number;
                Console.WriteLine($"100 / {number} = {result}");
            });
        }
        catch (AggregateException ex)
        {
            Console.WriteLine("Des erreurs ont été détectées dans la boucle parallèle !");

            // On parcourt toutes les exceptions qui sont arrivées
            foreach (var inner in ex.InnerExceptions)
            {
                Console.WriteLine($"Type : {inner.GetType().Name} — Message : {inner.Message}");
            }
        }
    }
}

Ce qui va se passer :

  • La collection contient des zéros, et diviser par 0 est tabou en math (et en C#) : il y aura des DivideByZeroException.
  • La boucle parallèle démarre le traitement. Dès qu'une itération fait la division par zéro — la boucle ne s'arrête pas immédiatement, les itérations déjà démarrées continuent.
  • Quand tous les threads auront fini (certains avec erreur, d'autres sans), une AggregateException sera levée vers l'extérieur avec toutes les exceptions.

Visualisons la mécanique de gestion des exceptions

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

Sur le schéma : différents threads peuvent rencontrer différentes erreurs, et toutes sont finalement "packagées" dans un unique AggregateException.

2. Aspects pratiques de la gestion d'erreurs

Que faire avec AggregateException ?

Quand on attrape AggregateException, il y a généralement deux scénarios :

  • Afficher à l'utilisateur (ou logger) toutes les erreurs pour en tirer des leçons.
  • Comprendre quelle erreur est critique et lesquelles sont mineures : décider si l'opération entière doit être considérée comme échouée ou si on peut ignorer certains échecs.

Pattern typique : traitement via Handle

try
{
    Parallel.For(0, 10, i =>
    {
        if (i == 3 || i == 7)
            throw new InvalidOperationException($"Erreur dans l'itération {i}");
        Console.WriteLine($"Traitée : {i}");
    });
}
catch (AggregateException ex)
{
    ex.Handle(e =>
    {
        if (e is InvalidOperationException)
        {
            Console.WriteLine("Erreur attrapée : " + e.Message);
            // true = l'erreur est considérée comme traitée
            return true;
        }
        // false = non traitée, sera relancée
        return false;
    });
}

Cette approche permet de traiter seulement les erreurs que vous considérez comme "normales", et de laisser remonter le reste pour ne pas masquer des pannes critiques.

Nuances intéressantes (et dangereuses)

Quand la boucle s'arrête ?
Quand une itération lève une exception, Parallel.For/ForEach n'enclenche pas de nouvelles itérations, mais celles déjà démarrées continuent. Après la fin de toutes les itérations actives, une AggregateException est lancée. Si beaucoup de threads sont actifs, la "queue" de travail en cours ira jusqu'au bout — d'où plusieurs erreurs possibles.

Si on n'attrape pas l'exception, l'application plantera.
Si vous n'encapsulez pas Parallel.For/ForEach dans un bloc try-catch, l'application terminera anormalement sur la première erreur rencontrée après la fin des itérations — pas très poli pour l'utilisateur.

Propager l'exception "à l'intérieur" de la boucle.
Parfois on veut éviter qu'une itération abîme tout le tableau : on peut gérer l'exception dans le corps de la boucle :

Parallel.ForEach(numbers, number =>
{
    try
    {
        int result = 100 / number;
        Console.WriteLine(result);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Erreur sur le nombre {number} : {ex.Message}");
    }
});

Cette méthode est utile si vous ne voulez pas accumuler toutes les exceptions dans un seul paquet — vous gérez chaque échec localement (par ex. en loggant). Mais attention : si vous faites ça, aucune AggregateException ne sera levée, et vous ne saurez pas facilement si l'ensemble du traitement s'est bien passé.

Si Break() ou Stop() sont appelés.
Si une itération appelle ParallelLoopState.Break() ou ParallelLoopState.Stop(), la boucle tente d'empêcher de nouvelles itérations : Break() termine les itérations après l'index courant, Stop() — toutes les itérations. Toutefois, si une exception survient en même temps, elle est conservée et renvoyée sous forme de AggregateException après la fin de toutes les itérations actives.

3. Nuances utiles

Exceptions dans boucles normales vs parallèles

Dans une boucle normale, n'importe quelle erreur provoque l'arrêt immédiat du travail : l'exception remonte et tout est bloqué.

Dans les boucles parallèles C# adopte une approche plus compromise : le travail continue pour les tâches déjà démarrées, et seulement à la fin toutes les erreurs "sortent" en une seule fois. Ça permet de collecter toutes les erreurs sans en perdre, et de décider après la fin de la boucle.

4. Erreurs typiques en travaillant avec les exceptions dans Parallel.For et Parallel.ForEach

Erreur n°1 : ignorer AggregateException.
Si vous n'attrapez pas AggregateException, l'application plantera à la fin des itérations, entraînant perte de données et crashs côté serveur ou GUI.

Erreur n°2 : utiliser .Wait() sans try-catch.
Appeler .Wait() pour Parallel.For/ForEach sans gérer AggregateException mènera à une exception non traitée, compliquant le diagnostic.

Erreur n°3 : ignorer les erreurs répétitives.
Des erreurs identiques répétées (par ex. division par zéro) peuvent surgir à cause de données répétées. Sans analyser InnerExceptions, on peut rater la cause racine.

Erreur n°4 : étouffer toutes les exceptions.
Utiliser catch (Exception) { /* vide */ } dans la boucle cache les erreurs, causant perte d'infos et bugs "fantômes".

Comportement des erreurs selon le type de boucle

Option For/foreach classique Parallel.For / ForEach
Exception est traitée Immédiatement Après la fin de toutes les itérations
Format de l'erreur Exception unique AggregateException avec collection
Autres itérations Ne s'exécutent pas Celles déjà lancées finissent
Attraper dans le corps Oui Oui
Attraper "à l'extérieur" Oui Oui, via AggregateException

"Astuces" et petites questions d'entretien :

  • Que se passe-t-il si on n'attrape pas AggregateException ?
    L'application plantera après la fin des itérations — peu importe où et quand l'erreur est survenue.
  • Peut-on savoir dans quelle itération l'erreur est survenue ?
    Seulement si vous avez inclus vous-même l'info d'index ou de données dans l'exception.
  • Un AggregateException peut-il être vide ?
    Non, il est créé seulement s'il y a au moins une exception interne. S'il n'y a pas d'erreurs, il n'est pas lancé.
  • Les erreurs sont-elles traitées si elles sont catchées à l'intérieur de la boucle ?
    Oui, mais alors rien ne remontera vers l'extérieur et aucune AggregateException ne sera créée.

Maintenant vous êtes prêts non seulement à lancer des boucles multithread, mais aussi à gérer proprement leurs "accidents" parallèles ! Et, comme toujours — méfiez-vous du multithreading : il adore les surprises, surtout quand personne ne les attrape.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION