Parallélisme en C# : Comparaison entre Task.Run, Task.WhenAll et Parallel

Parallélisme en C# : Comparaison entre Task.Run, Task.WhenAll et Parallel

Dans le développement C#, plusieurs mécanismes permettent d'exécuter des tâches en parallèle. Deux approches courantes sont l'utilisation de Task.Run combiné à Task.WhenAll d'une part, et la classe Parallel d'autre part. Bien que toutes deux visent à améliorer les performances par l'exécution concurente, leurs cas d'usage et leur fonctionnement interne diffèrent.

  1. Utilisation de Task.Run avec Task.WhenAll

Cette méthode est particulièrement adaptée aux opérations intensives en entrées/sorties (I/O) comme les requêtes réseau ou les accès fichiers, ainsi qu'aux scénarios où la programmation asynchrone est privilégiée. Elle consiste à démarrer plusieurs tâches de manière concurrente puis à attendre leur achèvement collectif.

Cas d'application typiques :

  • Opérations asynchrones impliquant async/await.
  • Tâches indépendantes nécessitant peu de partage de ressources (sinon, attention à la sécurité des threads).
  • Nombre de tâches modéré, car la création excessive peut entraîner une surcharge des ressources.
  1. Clasce Parallel

La classe Parallel (via Parallel.For ou Parallel.ForEach) est conçue pour le parallélisme de données, idéale pour les traitements intensifs en calcul CPU. Elle exploite le pool de threads pour traiter simultanément les éléments d'une collection et se comporte de manière synchrone (bloquant le thread appelant).

Cas d'application typiques :

  • Calculs lourds en CPU, tels que le traitement d'images ou les simulations numériques.
  • Traitement parallèle d'éléments d'une collection, où chaque traitement est indépendant ou gère un état partagé de façon thread-safe.
  • Lorsque l'opération est synchrone et qu'on souhaite tirer parti des cœurs multiples du processeur.

Tableau comparatif

Caractéristique Task.Run + Task.WhenAll Parallel
Scénario idéal Opérations I/O, tâches asynchrones, tâches indépendantes Calculs intensifs CPU, parallélisme de données
Contrôle de parallélisme Manuel (via liste de tâches) Automatique (configuration via ParallelOptions)
Blocage Non bloquant (async/await) Bloquant (opération synchrone)
Granularité Grossière (tâches indépendantes) Fine (éléments de collection)
Gestion des exceptions Via AggregateException Via AggregateException
Surcharge ressources Élevée (chaque tâche est planifiée indépendamment) Réduite (optimisation du pool de threads)

Recommandations de choix

  • Préférer Task.WhenAll pour :
    • Les opérations intensives en I/O (appels API, lectures/écritures fichiers).
    • Les attentes asynchrones de résultats afin d'éviter de bloquer les threads.
    • Les tâches à logique indépendante et nombre dynamique.
  • Préférer Parallel pour :
    • Les calculs intensifs CPU (traitement d'images, calculs numériques).
    • Le traitement efficace de collections de données (tableaux, listes).
    • La limitation du degré de parallélisme via MaxDegreeOfParallelism.

Implémentation de l'annulation

Annulation avec Task.Run et Task.WhenAll

Utiliser CancellationTokenSource pour transmettre un jeton d'annulation aux tâches.


public async Task ExecuterTachesAvecAnnulationAsync()
{
    var jetonSource = new CancellationTokenSource();
    var listeTaches = new List<Task>();

    for (int compteur = 0; compteur < 10; compteur++)
    {
        listeTaches.Add(Task.Run(() => TraiterDonneesAsync(jetonSource.Token), jetonSource.Token));
    }

    jetonSource.CancelAfter(TimeSpan.FromSeconds(5)); // Annulation automatique après 5 secondes

    try
    {
        await Task.WhenAll(listeTaches);
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Les tâches ont été annulées.");
    }
}

private async Task TraiterDonneesAsync(CancellationToken jeton)
{
    while (!jeton.IsCancellationRequested)
    {
        await Task.Delay(1000, jeton); // Simulation d'une opération asynchrone
        jeton.ThrowIfCancellationRequested(); // Vérification de l'annulation
    }
}

Annulation avec Parallel

Passer un jeton d'annulation via ParallelOptions.


public void ExecuterParallelAvecAnnulation()
{
    var jetonSource = new CancellationTokenSource();
    var optionsParalleles = new ParallelOptions
    {
        CancellationToken = jetonSource.Token,
        MaxDegreeOfParallelism = 4 // Limite du nombre de threads simultanés
    };

    jetonSource.CancelAfter(TimeSpan.FromSeconds(5)); // Annulation après 5 secondes

    try
    {
        Parallel.For(0, 100, optionsParalleles, (indice, etat) =>
        {
            optionsParalleles.CancellationToken.ThrowIfCancellationRequested();
            EffectuerCalculIntensif(indice); // Simulation d'un traitement CPU
        });
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("La boucle parallèle a été annulée.");
    }
}

private void EffectuerCalculIntensif(int index)
{
    Thread.Sleep(500); // Simulation d'un travail CPU
}

Considérations importantes

  • Libération des ressources : Assurez-vous de libérer les ressources (connexions base de données, handles fichiers) après annulation.
  • Réactivité à l'annulation : Vérifiez régulièrement jeton.IsCancellationRequested dans les boucles ou opérations longues.
  • Agrégation des exceptions : Les deux mécanismes lèvent AggregateException, il faut itérer sur InnerExceptions pour gérer les erreurs spécifiques.
  • Compatibilité asynchrone : Parallel ne supporte pas les délégués asynchrones (il utilise des méthodes synchrones). Pour des boucles parallèles asynchrones, privilégiez Task.WhenAll.

Étiquettes: C# Task Task.Run Task.WhenAll Parallel

Publié le 19 juillet à 14h40