Parallelität und Multithreading in C++: Von den Grundlagen bis zu modernen Standards
Moderne Anwendungen erfordern zunehmend die gleichzeitige Ausführung mehrerer Aufgaben. Ob es sich um die Verarbeitung von Benutzeranfragen auf einem Server, Grafik-Rendering oder die Arbeit mit großen Datenmengen handelt – Parallelität und Multithreading in C++ sind zu unverzichtbaren Fähigkeiten für Entwickler geworden. In diesem Artikel behandeln wir die wichtigsten Mechanismen, die in der Standardbibliothek von C++ verfügbar sind, angefangen bei C++11 bis hin zu C++20. Sie erfahren, wie Sie Threads erstellen, ihre Arbeit synchronisieren und typische Fehler vermeiden.
Wenn Sie bereits mit den Grundlagen von C++ vertraut sind, aber tiefer in die Parallelität eintauchen möchten, ist dieses Material für Sie. Wir werden nicht in die Theorie der Betriebssysteme eintauchen, sondern uns auf praktische Werkzeuge konzentrieren: std::thread, std::mutex, std::async und atomare Operationen.
1. Grundlagen des Multithreadings: std::thread und Thread-Verwaltung
Mit C++11 wurde die standardmäßige Unterstützung für Threads durch die Klasse std::thread eingeführt. Sie können einen neuen Thread erstellen, indem Sie ein aufrufbares Objekt – eine Funktion, ein Lambda oder ein Funktor – an den Konstruktor übergeben.
#include <iostream>#include <thread>
void hello() { std::cout << "Hallo aus Thread " << std::this_thread::get_id() << std::endl;}
int main() { std::thread t(hello); // Thread erstellen t.join(); // Auf Beendigung des Threads warten return 0;}Es ist wichtig, für jeden erstellten Thread immer join() oder detach() aufzurufen, da das Programm sonst beim Zerstören des std::thread-Objekts abstürzt. join() blockiert den aktuellen Thread, bis der untergeordnete Thread beendet ist, während detach() dem Thread erlaubt, unabhängig weiterzulaufen.
Um Argumente an den Thread zu übergeben, verwenden Sie zusätzliche Parameter des Konstruktors:
#include <thread>#include <string>
void print_sum(int a, int b, const std::string& msg) { std::cout << msg << ": " << (a + b) << std::endl;}
int main() { std::thread t(print_sum, 3, 4, "Summe"); t.join(); return 0;}Beachten Sie: Die Argumente werden in den Thread hinein kopiert. Wenn Sie eine Referenz übergeben müssen, verwenden Sie std::ref().
2. Thread-Synchronisation: Mutexe und Sperren
Bei der Arbeit mit gemeinsamen Daten aus verschiedenen Threads entsteht ein Data Race (Datenwettlauf). Um dies zu vermeiden, wird Synchronisation verwendet. Das wichtigste Werkzeug ist std::mutex.
#include <iostream>#include <thread>#include <mutex>
std::mutex mtx;int shared_counter = 0;
void increment() { for (int i = 0; i < 100000; ++i) { mtx.lock(); ++shared_counter; mtx.unlock(); }}
int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Ergebnis: " << shared_counter << std::endl; return 0;}Der manuelle Aufruf von lock() und unlock() ist jedoch fehleranfällig: Wenn dazwischen eine Ausnahme auftritt, bleibt der Mutex gesperrt. Verwenden Sie besser die RAII-Wrapper std::lock_guard oder std::unique_lock:
void increment() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(mtx); ++shared_counter; } // wird beim Verlassen des Gültigkeitsbereichs automatisch entsperrt}Für eine flexiblere Steuerung (z. B. bedingte Sperrung) verwenden Sie std::unique_lock. Und wenn Sie mehrere Mutexe ohne Deadlock synchronisieren müssen, verwenden Sie std::lock().
3. Asynchrone Aufgaben: std::async und std::future
Es ist nicht immer praktisch, Threads manuell zu verwalten.