Concurrence en C# : comment faire en sorte que votre programme ne s'ennuie pas
\n\nImaginez que vous êtes un chef cuisinier. Vous avez trois plaques de cuisson, un four et un batteur. Si vous faites tout de manière séquentielle – d'abord couper les oignons, puis les faire revenir, puis monter la crème – le dîner s'éternisera. Il est bien plus efficace d'allumer le four pendant que vous coupez les légumes et de monter la crème pendant que la viande mijote. C'est ce qu'on appelle la concurrence : la capacité d'exécuter plusieurs tâches « simultanément » en basculant entre elles pour que les ressources ne restent pas inactives.
\n\nEn C#, vous disposez de trois outils principaux : les threads classiques (Thread), les tâches modernes (Task) avec async/await et le concept de goroutines, implémenté via les itérateurs. Dans cet article, nous allons décortiquer chaque approche, comparer leurs performances et vous montrer quand les utiliser. Si vous codez en C# et que vous voulez que votre code vole, pas qu'il rampe, vous êtes au bon endroit.
\n\nLes threads (Thread) : l'artillerie lourde
\n\nUn thread est le moyen le plus bas niveau d'organiser la concurrence. Chaque thread est un « processus léger » distinct au sein de votre programme, avec sa propre pile et son propre contexte. Créer un thread en C# est très simple :
\n\nusing System;\nusing System.Threading;\n\nclass Program\n{\n static void Main()\n {\n Thread thread = new Thread(DoWork);\n thread.Start();\n Console.WriteLine("Le thread principal travaille");\n thread.Join(); // on attend la fin\n }\n\n static void DoWork()\n {\n for (int i = 0; i < 5; i++)\n {\n Console.WriteLine($"Thread : {i}");\n Thread.Sleep(200);\n }\n }\n}\n\nIci, nous avons créé un thread séparé qui compte jusqu'à cinq, tandis que le thread principal affiche un message. Sans Join, le programme pourrait se terminer avant d'arriver à cinq.
Inconvénients des threads
\n- \n
- Lourdeur. Chaque thread réserve environ 1 Mo de mémoire pour la pile. Créer 1000 threads signifie occuper 1 Go pour rien. \n
- Complexité de synchronisation. Pour que deux threads ne corrompent pas les données partagées, il faut des verrous, des mutex et des sémaphores. Une erreur, et vous obtenez un deadlock ou une race condition. \n
- Changement de contexte. Le système d'exploitation perd du temps à basculer entre les threads. S'il y a plus de threads que de cœurs CPU, cela devient un goulot d'étranglement. \n
Les threads sont utiles lorsque vous avez besoin d'un véritable parallélisme sur un processeur multicœur pour des calculs lourds. Mais pour les entrées-sorties (fichiers, réseau, base de données), ils sont excessifs.
\n\nTask et async/await : le standard moderne
\n\nDepuis .NET 4.5, Microsoft a proposé une alternative élégante : la Task Parallel Library (TPL) et les mots-clés async/await. Il ne s'agit pas de threads à proprement parler, mais de promesses (promises) : vous dites au compilateur « lance ce travail, et quand il sera terminé, renvoie le résultat ». Pendant que le travail s'exécute, le thread n'est pas bloqué et retourne dans le pool.
using System;\nusing System.Net.Http;\nusing System.Threading.Tasks;\n\nclass Program\n{\n static async Task Main()\n {\n Console.WriteLine("Début du téléchargement...");\n string data = await DownloadDataAsync();\n Console.WriteLine($"Téléchargé : {data.Length} caractères");\n }\n\n static async Task<string> DownloadDataAsync()\n {\n using HttpClient client = new HttpClient();\n string result = await client.GetStringAsync("https://example.com");\n return result;\n }\n}\n\nRemarquez : await ne bloque pas le thread. Pendant que les données sont téléchargées sur le réseau, le thread retourne dans le pool et peut servir d'autres requêtes. C'est idéal pour les serveurs web et les applications GUI – l'interface ne se fige pas.
Comment ça marche sous le capot ?
\nQuand le compilateur voit async, il transforme la méthode en machine à états. À chaque await, la méthode « mémorise » son état