C++20: Mehr Details zu Coroutinen
Weiter mit dem Überblick zu Coroutinen: In C++20 wird es jedoch keine Coroutinen geben, sondern ein Framework, um Coroutinen zu implementieren.
Nachdem der letzte Artikel "C++20: Coroutinen – ein erster Überblick [1]" in Coroutinen einführte, geht es heute um weitere Details. Gerne möchte ich wiederholen: Wir erhalten in C++20 keine Coroutinen, sondern ein Framework, um Coroutinen zu implementieren.
Mein Ziel in diesem und weiteren Artikeln ist es, dieses Framework zum Implementieren eigener Coroutinen zu erklären. Am Ende kannst du Coroutinen erzeugen oder existierende Implementierungen von Couroutinen wie die exzellente cppcoro- [2]Umsetzung von Lewis Baker verwenden.
Der heutige Artikel ist ein Weder-noch-Artikel. Weder stellt dieser Artikel einen Überblick dar, noch steigt er tief in das Coroutinen-Framework ein.
Die erste Frage, die du zu Coroutinen hast, wird wohl sein: Wann sollten Coroutinen verwendet werden?
Typische Anwendungsfälle
Coroutinen werden gerne für Event-getriebene Applikationen [3] verwendet. Diese können eine Simulation, ein Spiel, ein Server, ein Benutzerinterface oder auch ein Algorithmus sein. So schrieb ich zum Beispiel vor ein paar Jahren einen Simulator für einen Defibrillator. Der Defibrillator kam vor allem für die klinischen Usability-Tests zum Einsatz und ist eine Event-getriebenen Applikation. Daher setze ich ihn mithilfe des Event-getriebenen Frameworks twisted [4] in Python um.
Coroutinen werden auch gerne für kooperatives Multitasking eingesetzt. Die zentrale Idee des kooperativen Multitaskings ist es, dass sich jeder Task so viel Zeit nimmt, wie er benötigt. Kooperatives Multitasking unterscheidet sich vom präemptiven Multitasking darin, dass ein Scheduler entscheidet, wie lange jeder Task die CPU erhält. Kooperatives Multitasking erlaubt es, Concurrency einfacher umzusetzen, da ein Task nicht in einem kritischen Bereich unterbrochen wird. Wenn du mehr Aufklärung zu den Begriffen kooperativ und präemptiv suchst, kann ich nur diesen exzellenten Überblickartikel empfehlen: "Cooperative vs. Preemptive: a quest to maximize concurrency power [5]".
Grundlegende Ideen
Coroutinen in C++20 sind asymmetrisch, first-class und stackless:
- Der Arbeitsablauf einer asymmetrischen Coroutinen geht zum Aufrufer zurück.
- First-class-Coroutinen verhalten sich wie Daten. "Verhalten wie Daten" meint, das sich diese Coroutinen als Argument oder Rückgabewert einer Funktion verwenden lassen oder in einer Variable gespeichert werden können.
- Eine Stackless-Coroutine erlaubt es, die Top-Level-Coroutinen zu pausieren und wieder zu starten. Die Ausführung der Coroutinen und deren Wert geht an den Aufrufer der Coroutine. Im Gegensatz dazu reserviert eine Stackful-Coroutine einen Stack von 1 MByte auf Windows und 2 MByte auf Linux.
Designziele
Gor Nishanov, der maßgeblich an der Standardisierung von Coroutinen in C++ beteiligt ist, stellt die Designziele von Coroutinen vor. Sie sollen
- be highly scalable (to billions of concurrent coroutines).
- have highly efficient resume and suspend operations comparable in cost to the overhead of a function.
- seamlessly interact with existing facilities with no overhead.
- have open-ended coroutine machinery allowing library designers to develop coroutine libraries.
- exposing various high-level semantics such as generators, goroutines, tasks and more.
- usable in environments where exceptions are forbidden or not available.
Zur Coroutinen werden
Eine Funktion, die die Schlüsselworte co_return
, co_yield
oder co_return
verwendet, wird automatisch zur Coroutine:
co_return
: Eine Coroutine verwendetco_return
als Rückgabeanweisung.co_yield
: Dankco_yield
lässt sich ein unendlicher Datenstrom implementieren, von dem sukzessive der Wert angefragt werden kann. Der Rückgabetyp der FunktiongeneratorForNumbers(int begin, int inc = 1)
, die ich in dem letzten Artikel ("C++20: Coroutinen – ein erster Überblick [6]") vorgestellt habe, ist ein Generator. Ein Generator besitzt einen speziellen Promisepro
, sodass ein Aufrufco_yield i
äquivalent zupro.yield_value(i)
ist. Unmittelbar nach dem Aufruf wird die Coroutinte schlafen gelegt.co_await
:co_await
führt eventuell dazu, dass die Ausführung einer Coroutine angehalten oder wieder aufgenommen wird. Der Ausdruckexp
inco_await
exp
ist ein sogenannter awaitable-Ausdruck sein. Dazu setztexp
ein spezifisches Interface um. Es besteht aus den Funktionenawait_ready
,await_suspend
undawait_resume
.
Zwei Awaitables
Der C++20-Standard definiert bereits zwei Awaitables als elementare Bausteine: std::suspend_always
und std::suspend_never
.
std::suspend_always:
struct suspend_always {
constexpr bool await_ready() const noexcept { return false; }
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
std::suspend_always
pausiert immer, da await_ready false
zurückgibt. Genau das Gegenteil gilt für das zweite Awaitable std::suspend_never.
std::suspend_never:
struct suspend_never {
constexpr bool await_ready() const noexcept { return true; }
constexpr void await_suspend(coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
Ich hoffe, dass das folgende Beispiel es einfacher macht, diese Theorie zu verdauen. Ein Server ist das "hello world"-Beispiel für eine Coroutine.
Ein blockierender und ein wartender Server
Ein Server ist eine Event-getriebene Applikation. Er wartet typischerweise in einer Evenschleife auf Clientanfragen.Der folgende Codeschnipsel stellt die Struktur eines einfachen Servers vor:
Acceptor acceptor{443}; // (1)
while (true){
Socket socket= acceptor.accept(); // blocking (2)
auto request= socket.read(); // blocking (3)
auto response= handleRequest(request);
socket.write(response); // blocking (4)
}
Der sequenzielle Server beantwortet jede Clientanfrage in demselben Thread. Der Server lauscht auf den Post 443 (Zeile 1), nimmt jede Verbindung an (Zeile 2), liest die ankommenden Daten von dem Client (Zeile 3) ein und schickt seine Antwort an den Client zurück (Zeile 4). Die Aufrufe in den Zeilen 2 bis 4 sind blockierend.
Dank co_await
lassen sich die blockierenden Aufrufe einfach pausieren und wieder aufnehmen. Der ressourcenintensive blockierende Server wird dadurch zum ressourcenschonenden wartenden Server:
Acceptor acceptor{443};
while (true){
Socket socket= co_await acceptor.accept();
auto request= co_await socket.read();
auto response= handleRequest(request);
co_await socket.write(response);
}
Du vermutest es wohl schon. Der entscheidende Ausdruck, um Coroutinen zu verstehen, sind die awaitable-Ausdrücke expr
in co_await expr
. Sie müssen die Funktionen await_ready
, await_suspend
und await_resume
umsetzen.
Wie geht's weiter?
Das Framework für das Schreiben von Coroutinen besteht aus mehr als 20 Funktionen. Diese gilt es zumindest teilweise zu implementieren oder zur überladen. Mit meinem nächsten Artikel tauche ich tiefer in das Framework ein.
Online-C++-Schulungen und ein paar persönliche Worte
Aufgrund des Coronavirus biete ich alle meine Schulungen jetzt auch online an. Ich halte bereits seit mehr als 10 Jahren Online-Seminare. Dank der modernen Webkonferenz-Werkzeuge ist ein Online-Seminar mehr als ein Ersatz für eine Präsenzschulung. Ein Online-Seminar bietet Mehrwert gegenüber einer Präsenzschulung an.
Online-Seminare:
- C++11/14: 18. bis 20. Mai 2020 [7]
- Embedded-Programmierung mit modernem C++: 7. bis 9. Juli 2020 [8]
Wir sollten die aktuelle Krise als Chance sehen und nutzen, analoge Muster durch digitale Lösungen zu ersetzen und das Mehr an Zeit sinnvoll zu investieren. Eine Krise lässt sich nicht durch Aussitzen lösen. Ich habe die Preise für meine Online-Seminare während der Krise deutlich reduziert. Wem der Preis noch zu hoch ist, der kann direkt mit mir (schulung@ModernesCpp.de [9]) Kontakt aufnehmen. ( [10])
URL dieses Artikels:
https://www.heise.de/-4692662
Links in diesem Artikel:
[1] https://heise.de/-4687457
[2] https://github.com/lewissbaker/cppcoro
[3] https://github.com/lewissbaker/cppcoro
[4] https://twistedmatrix.com/trac/
[5] https://medium.com/traveloka-engineering/cooperative-vs-preemptive-a-quest-to-maximize-concurrency-power-3b10c5a920fe
[6] https://heise.de/-4687457
[7] https://www.modernescpp.de/index.php/c/2-c/25-c-11-und-c-14-online
[8] https://www.modernescpp.de/index.php/c/2-c/26-embedded-programmierung-mit-modernem-c-online
[9] mailto:schulung@ModernesCpp.de
[10] mailto:rainer@grimm-jaud.de
Copyright © 2020 Heise Medien