Concurrence et multithreading en C++ : guide complet

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Introduction à la concurrence et au multithreading en C++



Les applications modernes exigent des performances maximales. La concurrence et le multithreading en C++ permettent d'utiliser efficacement les processeurs multicœurs, accélérant l'exécution des tâches. Sans comprendre ces concepts, il est impossible de créer des systèmes à forte charge, des gestionnaires de données ou des moteurs de jeu.



Dans cet article, nous allons analyser les mécanismes clés du multithreading en C++, en commençant par les bases std::thread et en terminant par les motifs avancés. Vous apprendrez à éviter les courses de données, à synchroniser correctement les threads et à écrire du code parallèle sécurisé.



Fondamentaux du multithreading : std::thread et std::jthread



La classe std::thread (C++11) est le fondement de la programmation multithread. Elle permet de lancer une fonction ou un objet callable dans un thread séparé. Voici un exemple simple :



#include <iostream>#include <thread>

void worker(int id) { std::cout << "Thread " << id << " a commencé le travail"; std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "Thread " << id << " terminé";}

int main() { std::thread t1(worker, 1); std::thread t2(worker, 2); t1.join(); t2.join(); return 0;}


Points clés :

  • join() — le thread principal attend la fin du thread enfant
  • detach() — le thread continue son travail indépendamment (risque de fuite de ressources)
  • std::jthread (C++20) — appelle automatiquement join() dans le destructeur


Vérifiez toujours joinable() avant d'appeler join(), pour éviter une double jonction.



Synchronisation des threads : mutex et verrous



Le principal problème du multithreading est la course de données, lorsque plusieurs threads modifient simultanément une variable partagée. Les mutex sont utilisés pour la protection.



std::mutex et std::lock_guard



#include <iostream>#include <thread>#include <mutex>

std::mutex mtx;int counter = 0;

void increment() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(mtx); ++counter; }}

int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Résultat : " << counter << ""; // 200000 return 0;}


std::lock_guard libère automatiquement le mutex en sortant de la portée. Pour une gestion plus flexible, utilisez std::unique_lock.



Deadlock et std::scoped_lock



Un interblocage (deadlock) se produit lorsque deux threads s'attendent mutuellement. La solution est d'utiliser std::scoped_lock (C++17), qui verrouille plusieurs mutex de manière atomique :



std::mutex m1, m2;

void safe_function() { std::scoped_lock lock(m1, m2); // travail sécurisé avec deux ressources}


Algorithmes parallèles et std::async



Pour exécuter des tâches asynchrones sans gestion manuelle des threads, utilisez std::async. Il retourne un std::future à partir duquel vous pouvez obtenir le résultat.



#include <iostream>#include <future>#include <chrono>

int compute(int n) { std::this_thread::sleep_for(std::chrono::seconds(n)); return n * n;}

int main() { auto future1 = std::async(std::launch::async, compute, 2); auto future2 = std::async(std::launch::async, compute, 3); std::cout << "Résultats : " << future1.get() << ", " << future2.get() << ""; // 4, 9 return 0;}


Politiques de lancement :

  • std::launch::async — lancement dans un nouveau thread
  • std::launch::deferred — exécution différée lors de l'appel à get()


Files d'attente thread-safe et variables de condition



Pour les modèles producteur-consommateur, utilisez std::queue avec un mutex et une variable de condition std::condition_variable.



#include <iostream>#include <thread>#include <mutex>#include <condition_variable>#include <queue>

std::queue<int> data_queue;std::mutex mtx;std::condition_variable cv;

void producer() { for (int i = 0; i < 5; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::lock_guard<std::mutex> lock(mtx); data_queue.push(i); cv.notify_one(); }}

void consumer() { while (true) { std::unique_lock<std::mutex> lock(mtx); cv.wait(lock, []{ return !data_queue.empty(); }); int value = data_queue.front(); data_queue.pop(); lock.unlock(); std::cout << "Valeur reçue : " << value << ""; if (value == 4) break; }}

int main() { std::thread prod(producer); std::thread cons(consumer); prod.join(); cons.join(); return 0;}


La variable de condition permet au consommateur d'attendre efficacement sans boucle active.



Atomiques et mémoire modèle



Pour les opérations simples sur des variables partagées, utilisez std::atomic. Il garantit des opérations sans verrou et un ordre mémoire correct.



#include <iostream>#include <thread>#include <atomic>

std::atomic<int> counter(0);

void increment() { for (int i = 0; i < 100000; ++i) { counter.fetch_add(1, std::memory_order_relaxed); }}

int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Résultat : " << counter.load() << ""; // 200000 return 0;}


Les ordres mémoire (memory_order_relaxed, memory_order_acquire, memory_order_release) contrôlent la visibilité des changements entre threads.



Bonnes pratiques et pièges courants



  • Évitez les verrous globaux — utilisez des verrous fins pour minimiser la contention
  • Préférez les algorithmes sans verrou lorsque c'est possible
  • Utilisez des outils de détection — ThreadSanitizer (TSan) pour trouver les courses de données
  • Évitez les deadlocks — verrouillez toujours les mutex dans le même ordre
  • std::jthread est préférable à std::thread pour la sécurité


La concurrence en C++ est un outil puissant, mais elle nécessite de la discipline. Commencez par des mécanismes simples comme std::async et std::jthread, puis passez progressivement à des constructions plus complexes.

Blogs

Book Recommendations