1. Introduction
En programmation, les débutants (et même les développeurs expérimentés) se mélangent souvent entre deux concepts similaires mais en réalité différents : multithreading et asynchronisme. Aux entretiens on aime poser cette question pour voir si la personne comprend la différence, car cela influence directement comment écrire du code rapide et réactif.
Voyons où est le piège.
Multithreading : quand beaucoup de mains travaillent
Multithreading — c'est l'organisation du travail d'un programme à l'aide de plusieurs threads. Un thread est un fil d'exécution, une "piste" séparée sur laquelle le processeur exécute les instructions du programme. Un processus (par exemple, notre application .NET) peut lancer plusieurs threads pour que différentes tâches s'exécutent en même temps.
Exemple de la vie quotidienne : Vous êtes chef de projet qui confie différentes tâches à plusieurs collègues, et toutes ces tâches sont faites simultanément. Par exemple, l'un écrit le rapport, un autre appelle le client, le troisième prépare la présentation.
Idée clé du multithreading : les tâches ont lieu réellement en parallèle (ou en quasi-parallèle si le processeur est unique, grâce au changement de contexte rapide).
Asynchronisme : savoir ne pas rester inactif
Asynchronisme — c'est organiser le code de façon à ce que le programme puisse faire autre chose pendant qu'il attend la fin d'une opération longue (par exemple, une réponse d'Internet ou la lecture d'un fichier). Le code asynchrone n'utilise pas forcément plusieurs threads ! Il ne bloque simplement pas l'exécution de votre programme pendant l'attente d'une opération.
Exemple de la vie quotidienne : Au lieu de rester devant la machine à café à regarder le café couler, vous lancez la machine et vous faites autre chose (répondez aux e-mails, lisez les news), puis vous récupérez votre café quand il est prêt.
Idée clé de l'asynchronisme : ne pas rester inactif en attendant des tâches "longues", mais faire quelque chose d'utile.
2. Quelle est la différence ?
Très souvent on utilise asynchronisme et multithreading ensemble, ce qui ajoute encore plus de confusion. Mais en réalité ces techniques répondent à des questions différentes :
- Multithreading sert à paralléliser réellement le travail en utilisant les processeurs/coeurs disponibles.
- Asynchronisme sert à organiser intelligemment l'attente des ressources (I/O réseau, I/O disque, etc.) sans bloquer le thread.
On peut les combiner, mais elles ne sont pas forcément liées.
Visualisation rapide
| Multithreading (Threads) | Asynchronisme (Async) | |
|---|---|---|
| Quand appliquer ? | Quand la tâche "consomme" le processeur (CPU-bound : calculs, rendu, traitement de tableaux) | Quand la tâche "attend" un événement (I/O-bound : réseau, disque, base de données) |
| Que fait le code ? | Sollicite le processeur, lance plusieurs threads, parallélise réellement | Quand il attend des données ("inactif"), il libère le thread — celui-ci peut faire autre chose |
| Que gère-t-il ? | Le nombre de threads exécutés simultanément | Qui est occupé/libre, et quoi faire quand l'opération termine |
| Exemple typique | Compression vidéo, rendu d'image | Téléchargement de fichier, requête HTTP vers un serveur |
Exemple 1 : Multithreading — calculons vite
Supposons que nous avons une tâche lourde : calculer la somme de grands nombres.
void ComputeSum(long start, long end)
{
long sum = 0;
for (long i = start; i <= end; i++)
{
sum += i;
}
Console.WriteLine($"Somme de {start} à {end} = {sum}");
}
// On lance trois tâches en même temps — chacune calcule sa partie
Thread t1 = new Thread(() => ComputeSum(1, 1000_000_000));
Thread t2 = new Thread(() => ComputeSum(1000_000_001, 2000_000_000));
Thread t3 = new Thread(() => ComputeSum(2000_000_001, 3000_000_000));
t1.Start();
t2.Start();
t3.Start();
// On attend la fin de tous les threads
t1.Join();
t2.Join();
t3.Join();
Pourquoi des threads ici ?
Parce que le travail est CPU-bound. Les threads sollicitent réellement le processeur, et si vous avez une machine multi-coeurs — le travail ira plus vite.
Exemple 2 : Asynchronisme — on attend la réponse du serveur
Le réseau est lent, et pendant qu'on attend la réponse du serveur, le thread peut être "libre".
// On télécharge une page web de façon asynchrone, le thread n'est pas bloqué
async Task DownloadPageAsync()
{
using HttpClient client = new HttpClient();
string html = await client.GetStringAsync("https://dotnet.microsoft.com/");
Console.WriteLine(html.Length);
}
Pourquoi l'asynchronisme ici ?
On ordonne au système "Commence le téléchargement", et on ne bloque pas le thread, on attend la notification quand les données arrivent.
3. Asynchronisme sans multithreading : mythe ou réalité ?
Question : Tout code asynchrone lance-t-il toujours un nouveau thread ?
Réponse : Non ! Souvent l'asynchronisme ne nécessite aucun thread supplémentaire.
Par exemple, quand vous faites await file.ReadAsync(...), .NET lance l'opération de façon asynchrone au niveau du système d'exploitation, et le thread qui a fait cet appel devient immédiatement "libre" et retourne au pool de threads. Quand l'opération finit, un thread libre du pool reprendra l'exécution de votre tâche.
- Si à la place de l'asynchronisme vous utilisez l'appel synchrone (file.Read(...)) — le thread attendrait bêtement la fin de l'opération sans rien faire.
- Le code asynchrone dit : "Processeur, pendant qu'on attend — occupe-toi d'autre chose !"
Illustration importante :
// Le "thread" n'est pas bloqué, il attend seulement que l'opération soit prête
await Task.Delay(1000); // Juste attendre une seconde — n'occupe pas le processeur !
Multithreading sans asynchronisme
Il arrive que paralléliser le travail ait du sens uniquement avec des threads : calculs lourds, grands boucles de traitement de données, etc. Dans ce cas l'asynchronisme est inutile pour accélérer les calculs eux-mêmes, car le processeur sera de toute façon utilisé à 100%.
Classique : traitement de gros fichiers
// Ce code sollicite vraiment le processeur — l'asynchronisme n'aidera pas.
void CalculateHash(string file)
{
byte[] data = File.ReadAllBytes(file); // synchrone !
// On calcule le hash...
}
Vous voulez accélérer — lancez plusieurs threads, chacun travaille sur son fichier.
4. À quoi ça ressemble dans votre application ?
Opérations asynchrones (await)
Dans notre application de cours on peut ajouter un chargement de données asynchrone. Par exemple, si vous demandez les taux de change ou la météo — mieux vaut le faire de manière asynchrone.
async Task GetWeatherAsync(string city)
{
using HttpClient client = new HttpClient();
string json = await client.GetStringAsync($"https://api.weather.com/{city}");
// On continue le travail quand la réponse est reçue
Console.WriteLine($"Météo à {city} : {json}");
}
Que se passe-t-il sous le capot ?
L'appel await "coupe" votre méthode en deux parties :
- On a lancé l'opération asynchrone — le thread OS est "libéré" et peut faire d'autres tâches.
- Quand les données sont prêtes — l'exécution de la méthode reprend sur un des threads libres du pool.
Multithreading pour des calculs lourds
Dans notre exemple avec la calculatrice (supposons qu'elle traite maintenant de grands tableaux de données) — c'est pertinent de lancer les calculs sur des threads séparés.
// On découpe la grosse tâche en petites parties, chacune calcule sa portion
List<Thread> threads = new List<Thread>();
for (int i = 0; i < 4; i++)
{
int rangeStart = i * 1000000;
int rangeEnd = (i + 1) * 1000000 - 1;
Thread t = new Thread(() => ComputeSum(rangeStart, rangeEnd));
threads.Add(t);
t.Start();
}
// On attend la fin de tous les threads
foreach (Thread t in threads) t.Join();
5. Erreurs typiques et nuances en travaillant avec l'asynchronisme
Erreur n°1 : utiliser async "pour la vitesse".
Très courant de croire que async accélère l'exécution du code. Ce n'est pas le cas. L'asynchronisme concerne la réactivité, pas la vitesse brute.
Si la tâche est CPU-bound (charge processeur) — l'asynchronisme ne la rendra pas plus rapide.
Si la tâche est I/O-bound (réseau, disque) — l'asynchronisme est utile parce que le thread ne reste pas inutilement inactif et peut faire d'autres choses.
Erreur n°2 : bloquer les threads via Wait() et Result.
Dans le code asynchrone il ne faut pas appeler Wait() ou la propriété Result sur les tasks. Cela conduit presque toujours à bloquer des threads et à des deadlocks.
// Mauvais ! Bloquera le thread et causera des problèmes
var result = GetDataAsync().Result;
async Task<string> GetDataAsync() { /* ... */ return "data"; }
L'approche correcte — utiliser await et ne pas bloquer le thread.
Erreur n°3 : asynchronisme et UI.
Dans les applications graphiques (WPF, WinForms) le problème principal est de ne pas geler le thread UI. Si vous lancez une opération longue ou bloquante sur le thread principal, toute l'application "se fige". L'asynchronisme résout ce problème : le travail lourd s'exécute en arrière-plan et l'interface reste réactive.
Erreur n°4 : absence d'une convention de nommage pour les méthodes asynchrones.
Si vous n'ajoutez pas le suffixe Async aux méthodes asynchrones, il est facile de confondre quelles méthodes sont synchrones et lesquelles sont asynchrones. Cela entraîne des blocages accidentels et des erreurs à l'appel. Nommez toujours les méthodes asynchrones avec Async à la fin.
GO TO FULL VERSION