Introduction : pourquoi la concurrence en C++ n'est pas effrayante, mais intéressante ?
\n\nIl y a longtemps, à l'ère des processeurs monocœurs, les programmes s'exécutaient strictement de manière séquentielle. Aujourd'hui, même votre smartphone possède 8 cœurs, et les serveurs en ont des dizaines. Faire travailler tous ces cœurs pour vous – voilà où se cache la véritable magie de la performance. Et C++, l'un des langages les plus puissants et de bas niveau, nous offre tout un arsenal d'outils pour cela. Mais comment ne pas s'y perdre ? Threads, coroutines (souvent appelées « goroutines » par analogie avec Go), async/await – que choisir ?
\n\nDans cet article, nous allons décortiquer la concurrence moderne en C++. Vous apprendrez quand utiliser les threads classiques et quand privilégier les coroutines légères de C++20, et comment async/await simplifie la programmation asynchrone. C'est parti !
\n\nClassique : le multithreading avec std::thread
\n\nCommençons par le plus évident : les threads. std::thread est un accès direct aux threads système du système d'exploitation. Chaque thread est un contexte d'exécution distinct qui peut fonctionner sur un cœur CPU séparé.
Exemple simple : calcul parallèle
\n\nImaginez que vous deviez calculer la somme des carrés des nombres de 1 à 10 millions. Séquentiellement, c'est long. Parallélisons :
\n\n#include <iostream>\n#include <thread>\n#include <vector>\n#include <numeric>\n\nvoid sum_squares(int start, int end, long long& result) {\n result = 0;\n for (int i = start; i <= end; ++i) {\n result += i * i;\n }\n}\n\nint main() {\n const int N = 10'000'000;\n const int NUM_THREADS = 4;\n std::vector<std::thread> threads;\n std::vector<long long> results(NUM_THREADS, 0);\n \n int chunk = N / NUM_THREADS;\n for (int i = 0; i < NUM_THREADS; ++i) {\n int start = i * chunk + 1;\n int end = (i == NUM_THREADS - 1) ? N : (i + 1) * chunk;\n threads.emplace_back(sum_squares, start, end, std::ref(results[i]));\n }\n \n for (auto& t : threads) {\n t.join();\n }\n \n long long total = std::accumulate(results.begin(), results.end(), 0LL);\n std::cout << "Somme des carrés : " << total << std::endl;\n return 0;\n}\n\n\nIci, nous créons 4 threads, chacun calcule sa partie de l'intervalle. Les résultats sont collectés dans un vecteur. Tout est simple et prévisible. Mais il y a des nuances :
\n\n- \n
- Frais généraux : la création de chaque thread coûte cher (allocation de pile, appels système). Pour des milliers de petites tâches, ce n'est pas efficace. \n
- Synchronisation : si les threads accèdent à des données partagées, des mutex sont nécessaires, ce qui peut provoquer des blocages et réduire les performances. \n
- Nombre limité : le nombre de threads ne doit généralement pas dépasser le nombre de cœurs CPU (en tenant compte de l'hyperthreading). \n
Pour les calculs lourds et longs, les threads sont un excellent choix. Mais pour les opérations d'E/S (lecture de fichiers, requêtes réseau), ils sont souvent excessifs. C'est là que les coroutines entrent en scène.
\n\nRévolution C++20 : les coroutines (goroutines)
\n\nLes coroutines en C++20 ne sont pas simplement une fonctionnalité de plus. C'est un changement fondamental dans la façon dont nous écrivons du code asynchrone. Contrairement aux threads, les coroutines sont des coroutines légères qui peuvent suspendre leur exécution et la reprendre plus tard, sans bloquer le thread du système d'exploitation.
\n\nDe nombreux développeurs les appellent « goroutines » en raison de leur similitude avec les goroutines en Go, bien que techniquement ce soient des concepts différents. Le principal avantage : vous pouvez lancer des centaines de milliers de coroutines sur un seul thread sans perte de performance.
\n\nComment cela fonctionne-t-il ?
\n\nUne coroutine est une fonction qui contient le mot-clé co_await, co_return ou co_yield. Le compilateur la transforme automatiquement en un automate fini. Voici un exemple de base :
#include <iostream>\n#include <coroutine>\n#incl