CodeGym /Cours /C# SELF /Gestion de plusieurs tâches

Gestion de plusieurs tâches

C# SELF
Niveau 60, Leçon 2
Disponible

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
Task.WhenAll
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
Task.WhenAny
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.

2
Mission
C# SELF, niveau 60, leçon 2
Bloqué
Utilisation de Task.WhenAll pour exécuter plusieurs tâches
Utilisation de Task.WhenAll pour exécuter plusieurs tâches
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION