Parallelität und Multithreading in C++: Von den Grundlagen zu fortgeschrittenen Techniken
Moderne Anwendungen erfordern zunehmend maximale Leistung, insbesondere bei der Arbeit mit Mehrkernprozessoren. C++ bietet leistungsstarke Werkzeuge für die Arbeit mit Parallelität und Multithreading, von C++11 bis zu den neuesten Standards. In diesem Artikel behandeln wir fortgeschrittene Aspekte des Multithreadings: von der korrekten Verwendung von Mutexen und atomaren Operationen bis hin zu komplexen Synchronisationsmustern. Das Material richtet sich an Entwickler, die bereits mit den Grundlagen von Threads in C++ vertraut sind.
1. Thread-Verwaltung: std::thread und std::jthread
Die Klasse std::thread ist die Grundlage der Multithread-Programmierung in C++. Auf fortgeschrittenem Niveau ist es jedoch nicht nur wichtig, einen Thread zu erstellen, sondern auch seinen Lebenszyklus korrekt zu verwalten. Mit C++20 wurde std::jthread eingeführt, das sich beim Zerstören automatisch beitritt (join) und so Ressourcenlecks verhindert.
Beispiel für die Verwendung von std::jthread:
#include <iostream>#include <thread>#include <chrono>
void worker(int id) { std::this_thread::sleep_for(std::chrono::milliseconds(100 * id)); std::cout << "Thread " << id << " beendet";}
int main() { // std::jthread tritt beim Verlassen des Gültigkeitsbereichs automatisch bei std::jthread t1(worker, 1); std::jthread t2(worker, 2); std::jthread t3(worker, 3); // Kein expliziter Aufruf von join() oder detach() erforderlich // Beim Zerstören von t1, t2, t3 wartet main auf deren Beendigung std::cout << "Hauptthread arbeitet weiter"; return 0;}Wichtig zu beachten: std::jthread unterstützt auch den Stoppmechanismus über std::stop_token, was ein sicheres Beenden langer Hintergrundaufgaben ermöglicht.
2. Zugriffssynchronisation: Mutexe, Sperren und Bedingungsvariablen
Das Hauptproblem des Multithreadings sind Datenrennen. Zu deren Vermeidung werden Mutexe eingesetzt. Auf fortgeschrittenem Niveau wird die Verwendung von std::scoped_lock (C++17) empfohlen, um mehrere Mutexe ohne Deadlock-Risiko zu sperren.
Beispiel mit std::scoped_lock:
#include <iostream>#include <mutex>#include <thread>#include <vector>
std::mutex mtx1, mtx2;int shared_data = 0;
void safe_increment(int id) { // scoped_lock sperrt beide Mutexe in einer Reihenfolge, die Deadlocks vermeidet std::scoped_lock lock(mtx1, mtx2); ++shared_data; std::cout << "Thread " << id << " hat den Wert auf " << shared_data << " erhöht";}
int main() { std::vector<std::jthread> threads; for (int i = 0; i < 10; ++i) { threads.emplace_back(safe_increment, i); } return 0;}Für komplexere Synchronisationsszenarien (z. B. Producer-Consumer) werden Bedingungsvariablen (std::condition_variable) verwendet. Sie ermöglichen es Threads, auf das Eintreten eines bestimmten Ereignisses zu warten, ohne Ressourcen durch aktives Warten zu verschwenden.
3. Atomare Operationen und das C++-Speichermodell
Atomare Operationen (std::atomic) sind die Grundlage der lock-freien Programmierung. Sie garantieren, dass das Lesen und Schreiben einer Variable als eine einzige unteilbare Aktion ausgeführt wird. Auf fortgeschrittenem Niveau ist es jedoch wichtig, das C++-Speichermodell und die Stufen der Speicherordnung (memory ordering) zu verstehen.
Wichtige Stufen der Speicherordnung:
memory_order_relaxed— minimale Garantien, nur Atomaritätmemory_order_acquire— garantiert, dass alle nachfolgenden Lese- und Schreiboperationen die Ergebnisse vorheriger Operationen sehenmemory_order_release— garantiert, dass alle vorherigen Operationen nach diesem Schreibvorgang sichtbar sindmemory_order_acq_rel— Kombination aus acquire und release