Einführung in Parallelität und Multithreading in C++
Moderne Prozessoren verfügen über mehrere Kerne, und um deren volle Leistung zu nutzen, müssen Programme in der Lage sein, mehrere Aufgaben gleichzeitig auszuführen. Parallelität (Concurrency) ist die Fähigkeit eines Programms, mehrere Aufgaben in überlappenden Zeiträumen auszuführen, und Multithreading ist eine Möglichkeit, dies zu realisieren, bei der das Programm in mehrere parallel arbeitende Threads aufgeteilt wird.
In C++ wurde die Unterstützung für Multithreading mit dem Standard C++11 eingeführt und wird seitdem aktiv weiterentwickelt. In diesem Artikel werden wir alle wichtigen Aspekte behandeln: von der Erstellung von Threads bis hin zu komplexen Synchronisationsmustern.
Grundlagen: Erstellen und Verwalten von Threads
std::thread – der grundlegende Baustein
Die Klasse std::thread ermöglicht es, eine Funktion oder ein aufrufbares Objekt in einem separaten Thread zu starten. Betrachten wir ein einfaches Beispiel:
#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 starten t.join(); // Auf Thread-Ende warten return 0;}Die Methode join() zwingt den Hauptthread, auf die Beendigung des Kind-Threads zu warten. Eine Alternative ist detach(), das dem Thread erlaubt, unabhängig zu arbeiten, aber besondere Vorsicht erfordert (der Thread kann die ihn erstellenden Objekte überleben).
Argumentübergabe und Lambda-Funktionen
Threads können Argumente übergeben werden, und es können Lambdas verwendet werden:
#include <thread>#include <string>
void process(int id, const std::string& data) { // Datenverarbeitung}
int main() { std::thread t1(process, 1, "Daten"); std::thread t2([](int x) { // Lambda-Ausdruck }, 42); t1.join(); t2.join();}Thread-Synchronisation: Schutz gemeinsamer Daten
Wenn mehrere Threads auf dieselben Daten zugreifen, entsteht eine Race Condition. Um dies zu verhindern, werden Synchronisationsmechanismen verwendet.
Mutexe (std::mutex)
Ein Mutex ist ein Synchronisationsprimitiv, das garantiert, dass immer nur ein Thread ihn zu einem bestimmten Zeitpunkt besitzen kann:
#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; // Geschützter Zugriff }}
int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Counter: " << counter << std::endl; return 0;}std::lock_guard ist ein RAII-Wrapper, der den Mutex bei der Erstellung automatisch erfasst und bei der Zerstörung freigibt. Dies schützt davor, versehentlich zu vergessen, den Mutex freizugeben.
Bedingungsvariablen (std::condition_variable)
Bedingungsvariablen ermöglichen es Threads, auf das Eintreten eines bestimmten Ereignisses zu warten:
#include <queue>#include <condition_variable>
std::queue<int> data_queue;std::mutex queue_mutex;std::condition_variable cv;
void producer() { for (int i = 0; i < 10; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::lock_guard<std::mutex> lock(queue_mutex); data_queue.push(i); cv.notify_one(); // Verbraucher benachrichtigen }}
void consumer() { while (true) { std::unique_lock<std::mutex> lock(queue_mutex); cv.wait(lock, []{ return !data_queue.empty(); }); int value = data_queue.front(); data_queue.pop(); lock.unlock(); // Verarb