Concurrence et multithreading en C# : de Thread à async/await
Les applications modernes nécessitent de plus en plus souvent l'exécution simultanée de plusieurs tâches : du traitement des requêtes d'un serveur web aux calculs parallèles dans les programmes de bureau. En C# (.NET), il existe un arsenal puissant d'outils pour cela — des threads classiques (Thread) aux constructions de haut niveau async/await et à la bibliothèque Task Parallel Library (TPL). Dans cet article, nous allons examiner les concepts clés de la concurrence et du multithreading, leur application pratique et les meilleures pratiques.
Qu'est-ce que la concurrence et le multithreading ?
Avant de passer au code, il est important de comprendre la différence entre ces concepts :
- Concurrence (Concurrency) — la capacité d'un programme à exécuter plusieurs tâches en basculant entre elles, créant l'illusion d'une exécution simultanée. Cela peut être réalisé même sur un seul cœur de processeur.
- Multithreading (Multithreading) — une forme de concurrence où plusieurs threads (threads) s'exécutent physiquement en même temps sur un processeur multicœur.
- Parallélisme (Parallelism) — un sous-ensemble du multithreading où différentes parties d'une tâche s'exécutent strictement simultanément sur différents cœurs.
En C#, vous pouvez utiliser à la fois des threads de bas niveau et des abstractions qui gèrent automatiquement les threads. Le choix dépend de la tâche : pour les opérations I/O-bound (travail avec des fichiers, réseau), async/await est plus adapté, et pour les CPU-bound (calculs) — Parallel.For ou Task.
Création et gestion des threads : la classe Thread
La classe System.Threading.Thread est le moyen le plus bas niveau de travailler avec les threads en C#. Elle offre un contrôle total, mais nécessite une synchronisation minutieuse.
using System;using System.Threading;
class Program{ static void Main() { Thread thread = new Thread(DoWork); thread.Start();
// Le thread principal continue son travail for (int i = 0; i < 5; i++) { Console.WriteLine($"Thread principal : {i}"); Thread.Sleep(100); }
thread.Join(); // Attend la fin du thread enfant Console.WriteLine("Travail terminé."); }
static void DoWork() { for (int i = 0; i < 5; i++) { Console.WriteLine($"Thread enfant : {i}"); Thread.Sleep(150); } }}Problèmes de la gestion manuelle des threads :
- Complexité de la synchronisation (conditions de course, deadlocks).
- Frais généraux élevés pour la création et la destruction des threads.
- Absence de prise en charge intégrée du retour de résultat.
C'est pourquoi, dans le C# moderne, on préfère utiliser la Task Parallel Library.
Task Parallel Library (TPL) et async/await
La classe Task de l'espace de noms System.Threading.Tasks représente une opération asynchrone. Combinée aux mots-clés async et await, elle permet d'écrire du code asynchrone qui ressemble à du code synchrone.
Exemple de base avec Task
using System;using System.Threading.Tasks;
class Program{ static async Task Main() { Console.WriteLine("Début du travail.");
// Lance une tâche asynchrone Task<int> task = CalculateAsync(10);
// Pendant que la tâche s'exécute, on peut faire autre chose Console.WriteLine("Faire un autre travail...");
// Attend le résultat int result = await task; Console.WriteLine($"Résultat : {result}"); }
static async Task<int> CalculateAsync(int number) { await Task.Delay(1000); // Simulation d'une opération longue return number * number; }}