Nebenläufigkeit in C++: Von Threads zu Coroutinen und async/await

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

Einleitung: Warum Nebenläufigkeit in C++ nicht beängstigend, sondern interessant ist?

\n\n

Vor langer Zeit, im Zeitalter der Single-Core-Prozessoren, wurden Programme streng sequenziell ausgeführt. Heute hat selbst Ihr Smartphone 8 Kerne und Server haben Dutzende. All diese Kerne für sich arbeiten zu lassen – darin liegt die wahre Magie der Leistungssteigerung. Und C++, eine der mächtigsten und hardwarenächsten Sprachen, gibt uns dafür eine ganze Reihe von Werkzeugen an die Hand. Aber wie verliert man nicht den Überblick? Threads, Coroutinen (oft in Anlehnung an Go als „Goroutinen“ bezeichnet), async/await – was soll man wählen?

\n\n

In diesem Artikel ordnen wir die moderne Nebenläufigkeit in C++ systematisch ein. Sie erfahren, wann Sie klassische Threads und wann Sie die leichtgewichtigen Coroutinen von C++20 einsetzen sollten und wie async/await die asynchrone Programmierung vereinfacht. Los geht's!

\n\n

Der Klassiker: Multithreading mit std::thread

\n\n

Beginnen wir mit dem Offensichtlichsten – den Threads. std::thread bietet direkten Zugriff auf die System-Threads des Betriebssystems. Jeder Thread ist ein eigener Ausführungskontext, der auf einem separaten CPU-Kern arbeiten kann.

\n\n

Einfaches Beispiel: Parallele Berechnung

\n\n

Stellen Sie sich vor, Sie müssen die Summe der Quadrate der Zahlen von 1 bis 10 Millionen berechnen. Sequenziell dauert das lange. Lassen Sie uns parallelisieren:

\n\n
#include <iostream>\n#include <thread>\n#include <vector>\n#include <numeric>\n\nvoid sum_squares(int start, int end, long long& result) {\n    result = 0;\n    for (int i = start; i <= end; ++i) {\n        result += i * i;\n    }\n}\n\nint main() {\n    const int N = 10'000'000;\n    const int NUM_THREADS = 4;\n    std::vector<std::thread> threads;\n    std::vector<long long> results(NUM_THREADS, 0);\n    \n    int chunk = N / NUM_THREADS;\n    for (int i = 0; i < NUM_THREADS; ++i) {\n        int start = i * chunk + 1;\n        int end = (i == NUM_THREADS - 1) ? N : (i + 1) * chunk;\n        threads.emplace_back(sum_squares, start, end, std::ref(results[i]));\n    }\n    \n    for (auto& t : threads) {\n        t.join();\n    }\n    \n    long long total = std::accumulate(results.begin(), results.end(), 0LL);\n    std::cout << "Summe der Quadrate: " << total << std::endl;\n    return 0;\n}\n
\n\n

Hier erstellen wir 4 Threads, von denen jeder seinen Teil des Bereichs berechnet. Die Ergebnisse sammeln wir in einem Vektor. Alles einfach und vorhersagbar. Aber es gibt Nuancen:

\n\n
    \n
  • Overhead: Das Erstellen jedes Threads ist teuer (Stack-Zuweisung, Systemaufrufe). Für Tausende kleiner Aufgaben ist das ineffizient.
  • \n
  • Synchronisation: Wenn Threads auf gemeinsame Daten zugreifen, werden Mutexe benötigt, die Blockierungen verursachen und die Leistung beeinträchtigen können.
  • \n
  • Begrenzte Anzahl: Die Anzahl der Threads sollte normalerweise die Anzahl der CPU-Kerne (unter Berücksichtigung von Hyper-Threading) nicht überschreiten.
  • \n
\n\n

Für schwere, langwierige Berechnungen sind Threads eine ausgezeichnete Wahl. Aber für I/O-Operationen (Dateilesen, Netzwerkanfragen) sind sie oft überdimensioniert. Hier kommen Coroutinen ins Spiel.

\n\n

Die Revolution C++20: Coroutinen (Goroutinen)

\n\n

Coroutinen in C++20 sind nicht nur ein weiteres Feature. Sie sind eine grundlegende Veränderung in der Art und Weise, wie wir asynchronen Code schreiben. Im Gegensatz zu Threads sind Coroutinen leichtgewichtige Koroutinen, die ihre Ausführung anhalten und später wieder aufnehmen können, ohne den OS-Thread zu blockieren.

\n\n

Viele Entwickler nennen sie aufgrund der Ähnlichkeit mit Goroutinen in Go „Goroutinen“, obwohl es sich technisch um unterschiedliche Konzepte handelt. Der Hauptvorteil: Sie können Hunderttausende von Coroutinen auf einem einzigen Thread ausführen, ohne Leistungseinbußen.

\n\n

Wie funktioniert das?

\n\n

Eine Coroutine ist eine Funktion, die das Schlüsselwort co_await, co_return oder co_yield enthält. Der Compiler wandelt sie automatisch in einen endlichen Automaten um. Hier ist ein einfaches Beispiel:

\n\n
#include <iostream>\n#include <coroutine>\n#incl                

Blogs

Buchempfehlungen