Parallelität und Multithreading in C++: Fortgeschrittenes Niveau
\n\nModerne Anwendungen erfordern maximale Leistung, und Multithreading in C++ ist eines der Schlüsselwerkzeuge, um dies zu erreichen. Allerdings führen die grundlegenden Mechanismen (std::thread, std::mutex) oft zu Blockaden, Datenrennen und Effizienzeinbußen. In diesem Artikel betrachten wir fortgeschrittene Techniken für die Arbeit mit Parallelität in C++: von atomaren Operationen bis hin zu Lock-freien Datenstrukturen und asynchronen Aufgaben.
\n\n1. Atomare Operationen und Memory Ordering
\n\nKlassische Mutexe sind ein schwerfälliger Mechanismus. Für einfache Zähler oder Flags ist es besser, std::atomic zu verwenden. Es garantiert die Atomarität von Lese-/Schreibvorgängen ohne Sperren. Auf fortgeschrittenem Niveau ist es jedoch wichtig, das Memory Ordering zu verstehen – das C++-Speichermodell, das die Sichtbarkeit von Änderungen zwischen Threads definiert.
Die wichtigsten Speicherordnungen:
\n- \n
memory_order_relaxed– minimale Garantien, nur Atomarität. \n memory_order_consume– selten verwendet, datenabhängig. \n memory_order_acquire– alle nachfolgenden Lese-/Schreibvorgänge können nicht vor dieser Operation umgeordnet werden. \n memory_order_release– alle vorherigen Schreibvorgänge werden nach dieser Operation für andere Threads sichtbar. \n memory_order_acq_rel– Kombination aus Acquire und Release. \n memory_order_seq_cst– die strengste, globale sequenzielle Konsistenz (Standard). \n
Anwendungsbeispiel:
\n#include <atomic>\n#include <thread>\n#include <cassert>\n\nstd::atomic<bool> ready{false};\nint data = 0;\n\nvoid producer() {\n data = 42; // Daten schreiben\n ready.store(true, std::memory_order_release); // Flag veröffentlichen\n}\n\nvoid consumer() {\n while (!ready.load(std::memory_order_acquire)) // auf Flag warten\n ;\n assert(data == 42); // Garantiert data == 42 sehen\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\nHier stellen release und acquire sicher, dass das Schreiben von data vor dem Setzen des Flags erfolgt und das Lesen danach. Dies ist schneller als ein Mutex.
2. Lock-freie Datenstrukturen
\n\nLock-freie Algorithmen vermeiden Sperren durch die Verwendung von atomaren Operationen und CAS (Compare-And-Swap). Ein klassisches Beispiel ist ein Lock-freier Stack auf Basis einer einfach verketteten Liste.
\n\nImplementierung eines Lock-freien Stacks:
\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 // Schleife, bis CAS erfolgreich ist\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 // warten\n }\n if (old_head == nullptr) return false;\n result = old_head->data;\n // Achtung: In echtem Code wird ein Garbage Collector oder Hazard Pointer benötigt\n delete old_head;\n return true;\n }\n};\n\nWichtige Hinweise:
\n- \n