Concurrence et multithreading en C++ : niveau avancé
\n\nLes applications modernes exigent des performances maximales, et le multithreading en C++ est l'un des outils clés pour y parvenir. Cependant, les mécanismes de base (std::thread, std::mutex) entraînent souvent des blocages, des conditions de course et une perte d'efficacité. Dans cet article, nous explorerons les techniques avancées de gestion de la concurrence en C++ : des opérations atomiques aux structures de données lock-free et aux tâches asynchrones.
\n\n1. Opérations atomiques et memory ordering
\n\nLes mutex classiques sont un mécanisme lourd. Pour des compteurs ou des drapeaux simples, il est préférable d'utiliser std::atomic. Il garantit l'atomicité des lectures/écritures sans verrouillage. Cependant, à un niveau avancé, il est crucial de comprendre le memory ordering — le modèle mémoire du C++ qui détermine la visibilité des modifications entre les threads.
Les principaux ordres mémoire :
\n- \n
memory_order_relaxed— garanties minimales, atomicité uniquement. \n memory_order_consume— rarement utilisé, dépend des données. \n memory_order_acquire— toutes les lectures/écritures suivantes ne peuvent pas être réordonnées avant cette opération. \n memory_order_release— toutes les écritures précédentes deviennent visibles pour les autres threads après cette opération. \n memory_order_acq_rel— combinaison d'acquire et de release. \n memory_order_seq_cst— le plus strict, cohérence séquentielle globale (par défaut). \n
Exemple d'utilisation :
\n#include <atomic>\n#include <thread>\n#include <cassert>\n\nstd::atomic<bool> ready{false};\nint data = 0;\n\nvoid producer() {\n data = 42; // écriture des données\n ready.store(true, std::memory_order_release); // publication du drapeau\n}\n\nvoid consumer() {\n while (!ready.load(std::memory_order_acquire)) // attente du drapeau\n ;\n assert(data == 42); // garantie de voir data == 42\n}\n\nint main() {\n std::thread t1(producer);\n std::thread t2(consumer);\n t1.join();\n t2.join();\n return 0;\n}\n\nIci, release et acquire garantissent que l'écriture de data a lieu avant la définition du drapeau, et la lecture après. C'est plus rapide qu'un mutex.
2. Structures de données lock-free
\n\nLes algorithmes lock-free permettent d'éviter les verrouillages en utilisant des opérations atomiques et le CAS (Compare-And-Swap). Un exemple classique est la pile lock-free basée sur une liste chaînée simple.
\n\nImplémentation d'une pile lock-free :
\n#include <atomic>\n#include <memory>\n\ntemplate<typename T>\nclass LockFreeStack {\nprivate:\n struct Node {\n T data;\n Node* next;\n Node(const T& val) : data(val), next(nullptr) {}\n };\n std::atomic<Node*> head{nullptr};\n\npublic:\n void push(const T& val) {\n Node* new_node = new Node(val);\n new_node->next = head.load(std::memory_order_relaxed);\n while (!head.compare_exchange_weak(\n new_node->next, new_node,\n std::memory_order_release,\n std::memory_order_relaxed)) {\n // boucle jusqu'à ce que le CAS réussisse\n }\n }\n\n bool pop(T& result) {\n Node* old_head = head.load(std::memory_order_relaxed);\n while (old_head != nullptr &&\n !head.compare_exchange_weak(\n old_head, old_head->next,\n std::memory_order_acquire,\n std::memory_order_relaxed)) {\n // attente\n }\n if (old_head == nullptr) return false;\n result = old_head->data;\n // Attention : dans du code réel, un garbage collector ou des hazard pointers sont nécessaires\n delete old_head;\n return true;\n }\n};\n\nRemarques importantes :
\n- \n