1. Task.WhenAll : attendre toutes en même temps
Dans la vraie vie, il est rare que vous ne fassiez qu'une seule chose. Vous vous levez et regardez l'heure, vous prenez une douche en écoutant de la musique, vous vous habillez en préparant du café. Ou, si vous travaillez sur un ordi, vous téléchargez plusieurs fichiers, envoyez plusieurs requêtes réseau et calculez des données en parallèle. Toutes ces actions peuvent (et doivent !) être exécutées en parallèle pour ne pas faire perdre du temps à l'utilisateur. On ne veut pas attendre que chaque tâche se termine les unes après les autres ! Comment orchestrer tout ça ? Comment savoir quand TOUT est terminé ? Ou, au contraire, savoir qui a fini en premier ?
Task.WhenAll et Task.WhenAny sont les outils adaptés à ces scénarios.
Méthode WhenAll
Task.WhenAll est une méthode statique de la classe Task qui prend une collection de tâches et retourne une nouvelle tâche qui se termine seulement lorsque toutes les tâches fournies sont terminées. C'est comme si, à un examen, vous attendiez que tous vos camarades rendent leur sujet avant de quitter la salle.
Signature de la méthode
Task Task.WhenAll(params Task[] tasks)
Task<TResult[]> Task.WhenAll<TResult>(params Task<TResult>[] tasks)
Il existe des versions pour les tâches qui retournent un résultat (Task<TResult>) et pour les tâches "vides" (Task).
Exemple simple : attendre le téléchargement de tous les fichiers
Essayons de télécharger trois fichiers de manière asynchrone. Pour simplifier — on simule le téléchargement avec un délai (Task.Delay). Les exemples sont réels et le code fonctionne.
using System;
using System.Threading.Tasks;
class Program
{
// Simule le téléchargement d'un fichier avec un délai, retourne le nom
static async Task<string> DownloadFileAsync(string fileName)
{
Console.WriteLine($"► Début du téléchargement : {fileName}");
await Task.Delay(2000); // On attend 2 secondes
Console.WriteLine($"✓ Téléchargement terminé : {fileName}");
return fileName;
}
static async Task Main()
{
var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
// Lance tous les téléchargements en même temps
var downloadTasks = new Task<string>[files.Length];
for (int i = 0; i < files.Length; i++)
{
downloadTasks[i] = DownloadFileAsync(files[i]);
}
// On attend la fin de tous les téléchargements
string[] results = await Task.WhenAll(downloadTasks);
Console.WriteLine($"Tous les téléchargements sont terminés ! Liste : {string.Join(", ", results)}");
}
}
Que se passe-t-il ici ?
- On n'attend pas chaque tâche séquentiellement. Les trois démarrent en parallèle (asynchrone).
- Task.WhenAll(downloadTasks) retourne une tâche qui ne se terminera que lorsque toutes les tâches auront terminé.
- Ensuite on peut travailler avec les résultats de toutes les tâches — les utiliser, les afficher, les transmettre ailleurs.
Analogie
C'est comme si vous aviez demandé à trois coursiers de livrer trois colis et que vous informiez le manager seulement quand tous sont arrivés chez les clients.
Schéma de fonctionnement de Task.WhenAll
sequenceDiagram
participant Program
participant Tâche1
participant Tâche2
participant Tâche3
Program->>Tâche1: Lancement
Program->>Tâche2: Lancement
Program->>Tâche3: Lancement
Tâche1-->>Program: Terminée (peut être avant les autres)
Tâche2-->>Program: Terminée
Tâche3-->>Program: Terminée
Program->>Program: Task.WhenAll terminé, tous les résultats sont disponibles
Que se passe-t-il si une des tâches finit avec une erreur ?
Task.WhenAll ne s'interrompt pas si une tâche échoue. Il attend la fin de toutes les tâches. Si au moins une d'entre elles a lancé une exception, la tâche résultante sera dans l'état Faulted, et elle contiendra la collection de toutes les exceptions qui se sont produites.
Exemple
static async Task<string> MayThrowAsync(string fileName)
{
await Task.Delay(500);
if (fileName == "fileB.txt")
throw new Exception("Erreur de téléchargement fileB.txt");
return fileName;
}
static async Task Main()
{
var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
var downloadTasks = files.Select(MayThrowAsync).ToArray();
try
{
string[] results = await Task.WhenAll(downloadTasks);
Console.WriteLine($"Tout va bien : {string.Join(", ", results)}");
}
catch (Exception ex)
{
Console.WriteLine($"Au moins une tâche s'est terminée avec une erreur : {ex.Message}");
}
}
En cas d'exception, pour voir toutes les erreurs internes, utilisez AggregateException.InnerExceptions.
Documentation officielle : Task.WhenAll docs
2. Task.WhenAny : attendre la première tâche terminée
Task.WhenAny est aussi une méthode statique, mais elle se termine dès qu'une des tâches fournies passe à l'état "Terminée" (que ce soit avec une erreur ou avec succès). Elle retourne une référence à la première tâche terminée.
Signature
Task<Task> Task.WhenAny(params Task[] tasks)
Task<Task<TResult>> Task.WhenAny<TResult>(params Task<TResult>[] tasks)
Analogie
Celui qui prépare le gâteau le plus vite a gagné, et on peut demander aux autres d'arrêter. Parfois il suffit de savoir quel serveur a répondu le plus rapidement et d'utiliser sa réponse.
Exemple : qui est le plus rapide ?
using System;
using System.Threading.Tasks;
class Program
{
// Simule une requête avec des vitesses différentes
static async Task<string> RequestAsync(string name, int delay)
{
await Task.Delay(delay);
return $"{name} s'est terminé en {delay} ms";
}
static async Task Main()
{
var taskA = RequestAsync("A", 1000);
var taskB = RequestAsync("B", 700); // Le plus rapide
var taskC = RequestAsync("C", 1500);
// On attend la première tâche terminée
Task<string> finished = await Task.WhenAny(taskA, taskB, taskC);
Console.WriteLine($"Le premier terminé : {finished.Result}");
}
}
Avec WhenAny on sait quelle tâche a gagné la course. Ensuite on peut annuler les autres tâches, par exemple avec un CancellationToken (sujet des leçons suivantes).
Pourquoi WhenAny retourne la tâche elle-même et pas le résultat ?
Parce que la méthode ne sait pas à l'avance quel type de résultat les tâches renverront : elle ne peut pas savoir laquelle finira la première. Donc elle renvoie la tâche — ensuite il faut récupérer le résultat soi-même : via await ou la propriété Result de cette tâche.
WhenAll vs WhenAny : illustration comparative
| Méthode | Quand elle déclenche | Ce qu'elle retourne | Scénario typique |
|---|---|---|---|
|
Quand toutes sont terminées | Tâche (Task/Task<T[]>) | On attend toutes les réponses ou tous les fichiers, on agrège les résultats |
|
Quand une des tâches est terminée | La tâche qui s'est terminée la première | On utilise le premier résultat, on annule les autres |
3. Combiner WhenAll et WhenAny — scénarios réels
Parfois il faut d'abord attendre n'importe quelle tâche, puis toutes. Ou l'inverse.
Exemple : "ping-pong" entre deux serveurs
Imaginez que vous envoyez des requêtes à deux serveurs clones au cas où l'un serait lent. Mais vous utilisez le résultat du premier.
static async Task Main()
{
var fastServer = RequestAsync("Serveur rapide", 400); // 400 ms
var slowServer = RequestAsync("Serveur lent", 2000); // 2000 ms
var completed = await Task.WhenAny(fastServer, slowServer);
Console.WriteLine(await completed);
// *Optionnel* : ici on peut annuler les tâches encore en cours via CancellationToken
}
Exemple : lancer le traitement seulement après le chargement de toutes les données
Dans votre appli, il y a trois sources de données. Ce n'est que quand les trois sont chargées qu'on passe au traitement :
static async Task Main()
{
var task1 = DownloadFileAsync("a.txt");
var task2 = DownloadFileAsync("b.txt");
var task3 = DownloadFileAsync("c.txt");
var all = await Task.WhenAll(task1, task2, task3);
ProcessFiles(all[0], all[1], all[2]);
}
4. Erreurs typiques en travaillant avec Task.WhenAll et Task.WhenAny
Erreur n°1 : attendre la fin de la première tâche avec WhenAll.
Les débutants pensent que Task.WhenAll se terminera dès qu'une tâche est finie. En réalité il attend la fin de toutes les tâches.
Erreur n°2 : ignorer les exceptions dans WhenAll.
Si une des tâches lance une exception, la tâche résultante sera en état Faulted. Si on ne gère pas AggregateException.InnerExceptions, on peut rater des erreurs importantes.
Erreur n°3 : accéder à Result sans vérification avec WhenAny.
Si la première tâche terminée a échoué, appeler Result lancera une exception. Vérifiez Task.IsFaulted avant d'y accéder.
Erreur n°4 : lancer les tâches séquentiellement au lieu de les exécuter en parallèle.
Par exemple, attendre chaque tâche avec await dans une boucle au lieu d'utiliser Task.WhenAll réduit fortement la performance.
GO TO FULL VERSION