CodeGym /Cours /C# SELF /Les classes Task et...

Les classes Task et Task<TResult>

C# SELF
Niveau 60 , Leçon 0
Disponible

1. Introduction

Dans le monde de la programmation asynchrone, on vit selon le principe du "déléguer avec feedback". Imagine : tu dois télécharger un fichier énorme, analyser des giga-octets de logs ou envoyer une requête à un serveur lointain. Plutôt que de rester figé en attente comme une statue, on dit au système : "Occupe-toi de ça, moi je bosse sur autre chose. Quand c'est fini — préviens-moi !"

C'est là que Task entre en scène — une incarnation élégante de cette approche. Ce n'est pas juste une abstraction technique, c'est un peu un "intérmédiaire intelligent" qui prend la responsabilité d'exécuter le job et garantit que le résultat ne se perde pas dans le vide numérique.

Task fonctionne comme un assistant personnel à qui tu confies une mission. Il hoche la tête, inscrit la tâche dans son carnet et dit : "Vas-y, occupe-toi de tes affaires, je te retrouverai quand ce sera prêt". Et il te retrouve — avec le résultat en main ou une explication honnête de pourquoi ça a foiré.

Si tu veux une analogie plus terre-à-terre, Task ressemble à un système moderne de prise de rendez-vous : tu t'inscris en ligne chez le médecin, tu reçois une confirmation, et tu n'as pas besoin de passer des heures dans la salle d'attente. Le système te rappelle quand ça approche, et toi tu peux vivre ta vie en attendant.

La classe Task

Task est le bloc de construction de base de la programmation asynchrone en .NET. Il représente une opération en cours ou future dont le résultat sera disponible plus tard. Si une méthode ne doit rien renvoyer, on utilise simplement Task.

public async Task BackupToCloudAsync()
{
    // Fait la magie du backup, ne retourne rien
}

La classe Task<TResult>

Si tu dois renvoyer un résultat (par exemple une chaîne, un nombre, un objet…), on utilise Task<TResult> :

public async Task<string> DownloadHtmlAsync(string url)
{
    // Télécharge la page et retourne le HTML
    return "<html>...</html>";
}

Pourquoi Task et pas Thread ?

Thread gère le thread lui-même (c'est lourd et dangereux), alors que Task est une abstraction de plus haut niveau : il peut s'exécuter dans le ThreadPool, peut fonctionner de façon asynchrone sans allouer un nouveau thread (par ex. pour les opérations I/O) et ne te force pas à gérer les détails bas niveau.

La classe Task te permet de dire : "Je veux lancer cette action", et c'est .NET qui décide comment elle sera exécutée.

2. Structure d'un objet Task

Propriétés et méthodes de Task à connaître

Propriété / Méthode Description
Status
État courant de la tâche
Result
Résultat pour Task<TResult> (bloque le thread)
IsCompleted
La tâche est-elle terminée
IsFaulted
La tâche a-t-elle levé une exception
IsCanceled
La tâche a-t-elle été annulée
Wait()
Bloque le thread courant jusqu'à la fin (dangereux)
ContinueWith()
Lancer une autre tâche après la fin
Exception
Accès à l'exception si le Task s'est terminé avec une erreur
Id
Identifiant unique de la tâche

Comment fonctionne une méthode asynchrone avec Task

sequenceDiagram
    participant Main as Main Thread
    participant Task as Task (Tâche en arrière-plan)
    Main->>Task: Lancement Task.Run(() => ...)
    Note right of Task: Exécution en arrière-plan
(CPU ou I/O) alt Tâche terminée Task->>Main: await terminé, on continue else Erreur Task->>Main: await lance une exception end

3. Création et lancement des tâches : comment marche Task

Méthodes async avec async

Le cas le plus courant — tu déclares la méthode async et tu retournes soit Task, soit Task<TResult> (comme on l'a vu).

Task.Run : exécution dans le ThreadPool

Si tu dois exécuter un boulot lourd en arrière-plan (par ex. faire des gros calculs ou encoder une vidéo), tu peux utiliser Task.Run :

Task work = Task.Run(() =>
{
    // Calculs lourds — on ne bloque pas le thread principal !
    Console.WriteLine("Les calculs en background ont commencé...");
    Thread.Sleep(2000); // Émulation d'un long travail
    Console.WriteLine("Les calculs en background sont terminés !");
});

Si tu veux obtenir un résultat :

Task<int> calculateTask = Task.Run(() =>
{
    // Par exemple, somme des 100 premiers nombres
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
});

Task.Factory.StartNew

C'est une façon plus bas-niveau et flexible, permet de paramétrer finement le démarrage de la tâche (par ex. indiquer un scheduler, passer des paramètres, etc.). Dans le code moderne, on recommande presque toujours d'utiliser Task.Run, plus simple et moins sujet aux erreurs.

4. Application du jour : notre catalogue de livres

Supposons qu'on ait une appli de catalogue de livres et qu'on doive ajouter une fonction pour charger des livres depuis une source "cloud" — ce sera une opération I/O-bound (requête HTTP lente ou lecture de fichier).

Ajoutons une méthode qui "charge" les livres de façon asynchrone (on émule un délai) :

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
}

public class BookCatalog
{
    public List<Book> Books { get; set; } = new();

    public async Task LoadBooksAsync()
    {
        Console.WriteLine("Chargement des livres...");
        await Task.Delay(2000); // Émulation d'un long chargement (par ex. HTTP ou fichier)
        Books = new List<Book>
        {
            new Book { Title = "CLR via C#", Author = "Jeffrey Richter" },
            new Book { Title = "C# in Depth", Author = "Jon Skeet" }
        };
        Console.WriteLine("Livres chargés avec succès.");
    }
}

Dans Main on appellera le chargement asynchrone (avec await) :

var catalog = new BookCatalog();
await catalog.LoadBooksAsync();
Console.WriteLine($"Le catalogue contient {catalog.Books.Count} livres.");

Tableau : Principales façons de créer et lancer un Task

Méthode de création Comment l'utiliser Résultat Usage
méthode async
async Task / async Task<T>
Opération asynchrone Souvent I/O, pratique
Task.Run
Task.Run(() => { ... })
Tâche en arrière-plan CPU-bound (calculs)
TaskCompletionSource<T>
On crée et complète le Task manuellement Contrôle total par le développeur Rare, pour des trucs bas-niveau

5. Cycle de vie d'un Task

Une Task peut être dans plusieurs états :

  • Created — la tâche est créée mais pas lancée (pour les Task avec démarrage explicite).
  • WaitingToRun — attend dans la file du ThreadPool.
  • Running — en cours d'exécution.
  • WaitingForActivation — attend d'être activée ou démarrée de l'extérieur.
  • RanToCompletion — terminée avec succès.
  • Faulted — terminée avec une erreur (exception).
  • Canceled — annulée (si l'annulation est supportée).

Diagramme

flowchart LR
    Start -->|Lancement de la tâche| Running
    Running -->|Succès| Completed
    Running -->|Erreur| Faulted
    Running -->|Annulation| Canceled

Testons ça en pratique

Task task = Task.Run(() =>
{
    Thread.Sleep(1000);
});
Console.WriteLine(task.Status); // Souvent : Running ou WaitingToRun
await task;
Console.WriteLine(task.Status); // RanToCompletion après la fin

6. Comment obtenir le résultat d'un Task<TResult> ?

Task<TResult> est un wrapper autour d'un résultat qui apparaîtra dans le futur. Quand tu dois attendre le résultat, utilise await :

Task<int> sumTask = Task.Run(() =>
{
    int sum = 0;
    for (int i = 1; i <= 5; i++) sum += i;
    return sum;
});

int result = await sumTask;
Console.WriteLine(result); // 15

Si tu oublies d'écrire await, tu obtiens un Task (une promesse), pas le résultat. C'est un piège asynchrone classique.

Alternative : Récupération synchrone du résultat (NE PAS FAIRE DANS L'UI !)

Parfois (par ex. dans des tests) il faut obtenir le résultat sans await. On peut utiliser la propriété .Result :

int result = sumTask.Result;

Mais si le Task n'est pas encore terminé, ce code bloque le thread, et si c'est le thread UI, l'application va geler ! Donc : préfère toujours await.

Erreurs typiques avec Task et Task<TResult>

Oublier de retourner un Task, la méthode devient void. Si la méthode n'a pas de valeur de retour — renvoie Task, pas void, sinon on ne pourra pas gérer les erreurs.

Ignorer await. Appeler simplement la méthode sans attendre, et la tâche vit sa vie ("fire and forget"). Tu ne sauras plus quand elle finit ni si elle a planté.

Attendre de façon bloquante via .Result ou .Wait(). Facile d'obtenir un deadlock, surtout dans l'UI et ASP.NET. Utilise uniquement await.

7. Fonctionnalités avancées de Task

Chaînage de tâches : ContinueWith

On peut "accrocher" des actions à exécuter après la fin d'une tâche via ContinueWith :

Task.Run(() => 10)
    .ContinueWith(t =>
    {
        Console.WriteLine($"Terminé ! Résultat : {t.Result}");
    });

Mais en C# moderne on fait généralement ça avec async/await — c'est plus lisible.

Exemple : Chargement parallèle et séquentiel de données

Supposons que tu doives charger deux livres depuis des sources différentes. Tu peux lancer les deux Task en parallèle et attendre les deux :

public async Task LoadBooksFromMultipleSourcesAsync()
{
    Task<List<Book>> t1 = LoadFromCloudAsync();
    Task<List<Book>> t2 = LoadFromLocalAsync();

    // On attend les deux tâches en parallèle
    await Task.WhenAll(t1, t2);

    // On combine les résultats
    Books = t1.Result.Concat(t2.Result).ToList();
}

private async Task<List<Book>> LoadFromCloudAsync()
{
    await Task.Delay(2000); // "Cloud"
    return new List<Book> { new Book { Title = "Cloud Book", Author = "Cloud Author" } };
}

private async Task<List<Book>> LoadFromLocalAsync()
{
    await Task.Delay(1000); // "Disque local"
    return new List<Book> { new Book { Title = "Local Book", Author = "Local Author" } };
}

Remarque : avec await sur Task.WhenAll(...), les deux requêtes démarrent en même temps et s'exécutent en parallèle (si possible), et l'appli attend que les deux soient terminées.

8. Nuances utiles

Task et Fire-and-forget

Parfois on veut lancer une tâche sans attendre sa fin (par ex. envoyer des logs au cloud ou "griller une toast" pendant que l'utilisateur bosse) :

async void LogToCloudAsync(string message)
{
    await Task.Run(() =>
    {
        // Envoi long du log
        Thread.Sleep(1000);
        Console.WriteLine($"Log envoyé : {message}");
    });
}

Mais souviens-toi : si une erreur survient dans cette tâche, il sera difficile de le savoir. Donc si possible, retourne un Task et au moins logue les exceptions à l'intérieur !

Task et Task<TResult> dans la vraie vie

  • Dans les applis clientes UWP/WPF/WinForms, ne bloque pas l'UI — utilise Task pour les opérations longues (fichiers, réseau).
  • Dans WebAPI/ASP.NET, Task aide à ne pas gaspiller des threads en attendant le réseau/BD, améliorant les perfs.
  • Organise l'exécution "parallèle" : télécharger, traiter et sauvegarder en même temps.
  • Presque toutes les méthodes longues ont une variante Async : File.ReadAllTextAsync, HttpClient.GetStringAsync, etc.

FAQ et points surprenants

Question : Pourquoi Task s'exécute parfois de façon synchrone ?
Réponse : Si l'opération est déjà terminée (par ex. résultat en cache), le compilateur et/ou le scheduler peut terminer la méthode sur le même thread de manière synchrone. C'est normal et ça accélère les appels répétés.

Question : Pourquoi ne pas utiliser async void ?
Réponse : Une telle méthode ne peut pas être "attendue", on ne peut pas capturer ses erreurs ni suivre sa fin. Utilise Task, et réserve async void aux EventHandler (par ex. Button_Click).

Question : Peut-on lancer plusieurs tâches et n'attendre que l'une d'elles ?
Réponse : Oui — utilise Task.WhenAny.

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