Parallelität und Multithreading in C++: Fortgeschrittenes Niveau

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

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ät
  • memory_order_acquire — garantiert, dass alle nachfolgenden Lese- und Schreiboperationen die Ergebnisse vorheriger Operationen sehen
  • memory_order_release — garantiert, dass alle vorherigen Operationen nach diesem Schreibvorgang sichtbar sind
  • memory_order_acq_rel — Kombination aus acquire und release

Blogs

Buchempfehlungen