Einführung in Nebenläufigkeit und Multithreading in C++
Moderne Anwendungen erfordern maximale Leistung. Nebenläufigkeit und Multithreading in C++ ermöglichen die effiziente Nutzung von Mehrkernprozessoren und beschleunigen die Ausführung von Aufgaben. Ohne das Verständnis dieser Konzepte ist es unmöglich, hochbelastete Systeme, Datenverarbeiter oder Spiele-Engines zu erstellen.
In diesem Artikel werden wir die wichtigsten Mechanismen des Multithreadings in C++ analysieren, angefangen bei den grundlegenden std::thread bis hin zu fortgeschrittenen Mustern. Sie erfahren, wie Sie Datenrennen vermeiden, Threads richtig synchronisieren und sicheren parallelen Code schreiben.
Grundlagen des Multithreadings: std::thread und std::jthread
Die Klasse std::thread (C++11) ist das Fundament der Multithread-Programmierung. Sie ermöglicht es, eine Funktion oder ein callable-Objekt in einem separaten Thread zu starten. Betrachten wir ein einfaches Beispiel:
#include <iostream>#include <thread>
void worker(int id) { std::cout << "Thread " << id << " hat Arbeit begonnen"; std::this_thread::sleep_for(std::chrono::seconds(1)); std::cout << "Thread " << id << " abgeschlossen";}
int main() { std::thread t1(worker, 1); std::thread t2(worker, 2); t1.join(); t2.join(); return 0;}Wichtige Punkte:
- join() — der Hauptthread wartet auf die Beendigung des Kind-Threads
- detach() — der Thread läuft unabhängig weiter (Risiko von Ressourcenlecks)
- std::jthread (C++20) — ruft automatisch join() im Destruktor auf
Überprüfen Sie immer joinable() vor dem Aufruf von join(), um ein doppeltes Verbinden zu vermeiden.
Thread-Synchronisation: Mutexe und Sperren
Das Hauptproblem des Multithreadings sind Datenrennen, wenn mehrere Threads gleichzeitig eine gemeinsame Variable ändern. Zum Schutz werden Mutexe verwendet.
std::mutex und 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 << "Ergebnis: " << counter << ""; // 200000 return 0;}std::lock_guard gibt den Mutex automatisch frei, wenn der Gültigkeitsbereich verlassen wird. Für eine flexiblere Steuerung verwenden Sie std::unique_lock.
Deadlock und std::scoped_lock
Ein Deadlock (Verklemmung) tritt auf, wenn zwei Threads aufeinander warten. Die Lösung ist die Verwendung von std::scoped_lock (C++17), das mehrere Mutexe atomar erfasst:
std::mutex m1, m2;
void safe_function() { std::scoped_lock lock(m1, m2); // sichere Arbeit mit zwei Ressourcen}Parallele Algorithmen und std::async
Zur Ausführung asynchroner Aufgaben ohne manuelle Thread-Verwaltung verwenden Sie std::async. Es gibt ein std::future zurück, aus dem das Ergebnis abgerufen werden kann.
#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 << "Ergebnisse: " << future1.get() << ", " << future2.get() << ""; // 4, 9 return 0;}Startrichtlinien:
std::launch::async— Start in einem neuen Threadstd::launch::deferred— verzögerte Ausführung im aufrufenden Thread