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 |
|---|---|
|
État courant de la tâche |
|
Résultat pour Task<TResult> (bloque le thread) |
|
La tâche est-elle terminée |
|
La tâche a-t-elle levé une exception |
|
La tâche a-t-elle été annulée |
|
Bloque le thread courant jusqu'à la fin (dangereux) |
|
Lancer une autre tâche après la fin |
|
Accès à l'exception si le Task s'est terminé avec une erreur |
|
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 | |
Opération asynchrone | Souvent I/O, pratique |
|
|
Tâche en arrière-plan | CPU-bound (calculs) |
|
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.
GO TO FULL VERSION