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.
- 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.
- 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.WhenAllpour :- 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
Parallelpour :- 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.IsCancellationRequesteddans les boucles ou opérations longues. - Agrégation des exceptions : Les deux mécanismes lèvent
AggregateException, il faut itérer surInnerExceptionspour gérer les erreurs spécifiques. - Compatibilité asynchrone :
Parallelne supporte pas les délégués asynchrones (il utilise des méthodes synchrones). Pour des boucles parallèles asynchrones, privilégiezTask.WhenAll.