1. Introduction
Il est temps de clarifier — en quoi Task et Thread diffèrent fondamentalement ? Pourquoi C# recommande depuis longtemps d'utiliser Task plutôt que de gérer les threads directement ? Dans quelles situations peut-on continuer à utiliser les threads manuellement, et quand il vaut mieux (et faut) rester en mode tasks ?
Si vous sentez que les mots "threads" et "tasks" commencent à se mélanger quelque part dans un coin sombre de votre cerveau et que votre coeur s'emballe — ne vous inquiétez pas, vous n'êtes pas seul. Même des développeurs expérimentés se plantent parfois quand on parle de parallélisme et d'asynchronisme.
On va tout mettre au clair. C'est parti !
Brève histoire de l'apparition de Task
Autrefois (avant .NET 4.0) le moyen évident d'exécuter du code en parallèle ou "en arrière-plan" était de créer un nouveau thread. Par exemple, new Thread(() => { ... }).Start(); Les threads sont beaux par leur simplicité. Mais ils sont pénibles parce que tout repose sur vos épaules. Allocation des ressources, cycle de vie, gestion des exceptions, synchronisation, monitoring, scalabilité — tout cela est la responsabilité du développeur. Et on cherche souvent plus de paresse, surtout en programmation !
Tout a changé avec l'arrivée des tâches — Task — dans l'espace de noms System.Threading.Tasks.Task. Une task n'est pas un thread. C'est une notion plus abstraite et flexible. Elle décrit un travail à effectuer un jour, éventuellement en parallèle.
2. Thread — "Thread nu"
Thread — c'est une unité d'exécution bas-niveau, représentant une portion dédiée de ressources du système d'exploitation (pile propre, contexte d'exécution, etc.). Si vous créez un thread manuellement, vous êtes responsable de son démarrage, de sa terminaison et de tous les aspects de sa vie.
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread thread = new Thread(() => {
Console.WriteLine("Bonjour depuis le thread !");
});
thread.Start();
thread.Join(); // On attend la fin du thread
}
}
- Ici on a créé un thread qui exécute la lambda sur sa propre pile.
- Après le démarrage du thread on appelle Join() pour attendre sa fin.
Quel est le piège ?
- Chaque thread consomme de la mémoire (pile, environ 1 Mo).
- En .NET il n'est pas recommandé de créer des milliers de threads à la main — le système en souffrira.
- Si on oublie d'appeler Join(), le thread principal peut se terminer avant le thread enfant, et le programme "coupe court".
- Les exceptions à l'intérieur d'un thread ne remontent pas automatiquement — il faut les attraper spécifiquement !
- Si on lance un thread — l'annuler "proprement" est difficile (pas de méthode Stop() !).
3. Task — "les tasks nouvelle génération"
Task — une abstraction plus intelligente qui représente "un travail qui sera fait un jour". Sous le capot, les tasks s'exécutent sur des pools de threads (ThreadPool), ce qui est bien plus efficace que de gonfler un nombre excessif de threads. Vous ne gérez pas leur création manuellement, le pool s'en charge et ajuste le nombre de threads selon la charge.
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Task task = Task.Run(() =>
{
Console.WriteLine("Bonjour depuis une Task !");
});
await task; // On attend la fin de la task
}
}
- Ici la task ne garantit pas qu'elle s'exécutera dans un thread séparé, mais habituellement elle tournera dans un thread du pool.
- Vous pouvez attendre la fin d'une task de la manière habituelle (await dans une méthode async ou task.Wait() en synchrone).
4. En quoi Task et Thread diffèrent ?
Décomposons clairement leurs différences, à quoi ils servent et quels sont les pièges (pas toujours évidents).
| Thread | Task | |
|---|---|---|
| Abstraction | Thread OS | Travail/Task (abstraction qui peut utiliser un thread) |
| Démarrage | Via new Thread(...).Start() | Via Task.Run(...), Task.Factory.StartNew(...), méthodes async |
| Contrôle direct | Oui (start, Join, priorité, etc.) | Non, .NET prend le contrôle |
| ThreadPool | Non, le thread est toujours créé neuf | Oui, utilise généralement ThreadPool |
| Gestion des ressources | Allocation d'une pile dédiée | Les ressources sont réutilisées par le pool |
| Scalabilité | Mauvaise : inefficace pour 1000+ threads | Excellente : des milliers de tasks = ok |
| Interaction | Thread distinct du point de vue OS | Peut continuer sur le thread courant, peut être sur le ThreadPool |
| Exceptions | Nécessite un try explicite, sinon elles peuvent "disparaître" | Les exceptions sont capturées dans la Task ; on peut les attraper avec await ou .Wait() |
| Annulation | Pas de mécanisme standard | Oui, support via CancellationToken |
| Récupération du résultat | Attendre via Join() | await, .Wait(), .Result |
| À utiliser pour | Cas spéciaux — threads UI, threads de longue durée | Presque toutes les tâches d'arrière-plan/parallèles |
5. Quand utiliser quoi ?
Quand utiliser Thread ?
Honnêtement, dans le code .NET moderne créer des threads manuellement est rarement nécessaire. Voici des exemples où c'est justifié :
- Il faut créer un thread qui va tourner très longtemps (par ex. la sérialisation d'un signal radio, ou le traitement de données venant d'un périphérique), et en plus il est "spécial" : priorité basse, culture d'exécution séparée, nom spécifique.
- Parfois pour l'intégration avec des API bas-niveau qui exigent un contrôle manuel des threads.
- Dans des cas très spécifiques, comme des planificateurs de tâches personnalisés.
Dans tous les autres cas — Task sera le choix plus correct et moderne.
Quand utiliser Task ?
Pratiquement toujours quand il faut faire du travail "en arrière-plan" ou "en parallèle" :
- Toute computation en arrière-plan qui peut tourner sur le pool de threads (par ex. traitement d'une requête sur un serveur, parsing de fichier, envoi d'e-mails).
- Lancement d'opérations asynchrones (async/await) — le mécanisme retourne Task ou Task<T>.
- Combinaison de tasks, gestion des continuations (continuations), travail en chaines.
- Facilité d'annulation, d'attente et de collecte des résultats : Task supporte CancellationToken, s'intègre bien aux API modernes.
- Opérations asynchrones I/O : requêtes réseau, fichiers, bases de données.
Comparaison
| Scénario | Thread | Task |
|---|---|---|
| Thread long-lived (par ex. son propre service) | Oui | Non |
| Exécution massive de tâches courtes | Non | Oui |
| Opérations I/O asynchrones (await) | Non | Oui |
| Combinaison, annulation, chaines de tâches | Non | Oui |
| Réglage fin de la priorité et de la culture | Oui (mais rare) | Non, seulement pour les tasks par défaut |
| Simple répartition du travail entre cores (CPU) | Parfois | Oui |
6. Nuances utiles
Task — ce n'est pas toujours un thread !
La magie la plus puissante : si vous utilisez Task pour des opérations I/O asynchrones, aucun thread supplémentaire n'est créé ! Tout "part dans le néant" (IO Completion Ports ou d'autres primitives plateforme). Le thread est libéré pendant que votre task attend quelque chose d'externe : fichier, réseau, base de données. En pratique, pendant l'attente, aucun thread n'est occupé !
Task et asynchronisme (I/O-bound) — la magie de await
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
// On télécharge de façon asynchrone le contenu d'un site (I/O-bound)
HttpClient client = new HttpClient();
string data = await client.GetStringAsync("https://www.dotnetfoundation.org");
Console.WriteLine($"Nombre de caractères reçus : {data.Length}");
}
}
- Ici la task (Task<string>) encapsule une opération I/O asynchrone.
- Le thread n'est pas bloqué — il continue de faire d'autres choses, et quand le téléchargement est fini l'exécution reprend.
- Créer manuellement un thread pour ce genre de travail est totalement superflu et inefficace.
Task et ThreadPool
Quand vous écrivez Task.Run(...) ou utilisez un API asynchrone (await), .NET utilise typiquement un pool de threads — le ThreadPool. C'est un ensemble de threads précréés qui "attendent sur le banc" et sont prêts à prendre une tâche rapidement. S'il y a peu de travail — les threads restent inactifs, s'il y a beaucoup de travail — de nouveaux threads sont créés automatiquement, mais de façon raisonnable ! Grâce à ça vos applications scalent en nombre de tâches sans surcharger le système.
Un thread créé via new Thread est presque toujours un "habitant" séparé du système — il ne retourne pas au pool après son exécution, il meurt simplement. C'est pour cela que Task est beaucoup plus efficace pour du parallélisme massif.
7. Erreurs typiques et pièges
Si vous décidez soudainement d'être un programmeur rétro et de tout écrire en threads, de belles aventures vous attendent : fuites mémoire, synchronisation compliquée, impossibilité d'annuler proprement le travail, threads "accrochés" (processus zombies), capture et traitement des erreurs via des API spéciales.
La chose la plus importante à retenir : "Task" — c'est pratique, sûr et moderne. Dans la grande majorité des cas quand on développe en C# aujourd'hui il n'y a aucune raison de revenir à la gestion manuelle des threads.
GO TO FULL VERSION