Jobs starten mit Coroutinen in C++20
C++20 besitzt drei neue Schlüsselwörter, um eine Funktion in eine Coroutine zu transformieren: co_return, co_yield und co_await. Letzteres benötigt ein Awaitable als Argument und startet den Awaiter-Arbeitsablauf. In diesem Artikel möchte ich auf die Awaitables genauer eingehen.
- Rainer Grimm
C++20 besitzt drei neue Schlüsselwörter, um eine Funktion in eine Coroutine zu transformieren: co_return
, co_yield
und co_await
. Letzteres benötigt ein Awaitable als Argument und startet den Awaiter-Arbeitsablauf. In diesem Artikel möchte ich auf die Awaitables genauer eingehen.
Den Inhalt dieses Artikels zu verstehen, setzt die Lektüre der vorherigen Artikel zu Coroutinen voraus, die sie aus der praktischen Perspektive beleuchten:
co_return
- Einfache Futures mit Coroutinen implementieren
- Lazy Futures mit Coroutinen in C++20
- Mit Coroutinen einen Future in einem eigenen Thread ausführen
co_yield
- Ein unendlicher Datenstrom dank Coroutinen in C++20
- Ein generischer Datenstrom mit Coroutinen in C++20
Vor der Thematisierung der Awaitables und ihrer Anwendung gilt es zuerst, auf den Awaiter-Arbeitsablauf einzugehen.
Der Awaiter-Arbeitsablauf
Der Awaiter-Arbeitsablauf basiert auf den Methoden des Awaitables: await_ready()
, await_suspend()
und await_resume()
. C++20 bietet zwei elementare Awaitables an: std::suspend_always
und std::suspend_never
. Beide habe ich in den vorherigen Artikeln bereits eingesetzt.
std::suspend_always
struct suspend_always {
constexpr bool await_ready() const noexcept { return false; }
constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
std::suspend_never
struct suspend_never {
constexpr bool await_ready() const noexcept { return true; }
constexpr void await_suspend(std::coroutine_handle<>) const noexcept {}
constexpr void await_resume() const noexcept {}
};
Dies ist der Awaiter-Arbeitsablauf "in Prosa":
awaitable.await_ready() returns false: // (1)
suspend coroutine
awaitable.await_suspend(coroutineHandle) returns: // (3)
void: // (4)
awaitable.await_suspend(coroutineHandle);
coroutine keeps suspended
return to caller
bool: // (5)
bool result = awaitable.await_suspend(coroutineHandle);
if result:
coroutine keep suspended
return to caller
else:
go to resumptionPoint
another coroutine handle: // (6)
auto anotherCoroutineHandle =
awaitable.await_suspend(coroutineHandle);
anotherCoroutineHandle.resume();
return to caller
resumptionPoint:
return awaitable.await_resume();
Der Arbeitsablauf wird nur dann ausgeführt, wenn awaitable.await_ready()
false
(Zeile 1) zurückgibt. Falls der Funktionsaufruf hingegen true
ergibt, ist die Coroutine bereits fertig und gibt als Ergebnis den Wert von awaitable.await_resume()
(Zeile 2) zurück.
Lass mich daher annehmen, dass false
zurückgegeben wird. Dann wird die Ausführung der Coroutine zuerst pausiert (Zeile 3) und sofort das Ergebnis des Aufrufs awaitable.await_suspend()
ausgewertet. Der Rückgabewert kann void
(Zeile 4), ein Wahrheitswert (Zeile 5) oder ein anderes Coroutinen-Handle (Zeile 6) wie anotherCoroutineHandle
sein. Abhängig vom Rückgabewert wird der Kontrollfluss mit dem Aufrufer oder eine andere Coroutine fortgesetzt.
Einen Job auf Anfrage starten
Die Coroutine im folgenden Beispiel ist so einfach wie möglich gehalten. Sie wartet mithilfe des vordefinierten Awaitables std::suspend_never()
:
// startJob.cpp
#include <coroutine>
#include <iostream>
struct Job {
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
handle_type coro;
Job(handle_type h): coro(h){}
~Job() {
if ( coro ) coro.destroy();
}
void start() {
coro.resume(); // (6)
}
struct promise_type {
auto get_return_object() {
return Job{handle_type::from_promise(*this)};
}
std::suspend_always initial_suspend() { // (4)
std::cout << " Preparing job" << '\n';
return {};
}
std::suspend_always final_suspend() noexcept { // (7)
std::cout << " Performing job" << '\n';
return {};
}
void return_void() {}
void unhandled_exception() {}
};
};
Job prepareJob() { // (1)
co_await std::suspend_never(); // (2)
}
int main() {
std::cout << "Before job" << '\n';
auto job = prepareJob(); // (3)
job.start(); // (5)
std::cout << "After job" << '\n';
}
Mancher mag vielleicht denken, dass die Coroutine prepareJob
(Zeile 1) sinnfrei ist, da das Awaitable immer pausiert. Mitnichten, die Funktion prepareJob
ist zumindest eine Coroutinen-Fabrik, die co_await
(Zeile 2) verwendet und ein Coroutinen-Objekt zurückgibt. Die Funktion createJob()
in Zeile 3 erzeugt ein Coroutinen-Objekt vom Typ Job
. Bei der Analyse der Coroutine Job
fällt auf, dass die Coroutine sofort pausiert wird, da die Methode initial_suspend
des Promise den Awaitable std::suspend_always
(Zeile 5) zurückgibt. Das ist genau der Grund, warum der Funktionsaufruf job.start
(Zeile 5) notwendig ist, um die Coroutine aufzuwecken (Zeile 6). Die Methode final_suspend
(Zeile 7) gibt ebenfalls std::suspend_always
zurück.
Das Programm startJob.cpp
ist ein idealer Startpunkt für weitere Experimente. Es hilft deutlich dem Verständnis, wenn sich der Arbeitsablauf direkt nachvollziehen lässt.
Der transparente Arbeitsablauf
Dem vorherigen Programm habe ich einige Kommentare hinzugefügt:
// startJobWithComments.cpp
#include <coroutine>
#include <iostream>
struct MySuspendAlways { // (1)
bool await_ready() const noexcept {
std::cout << " MySuspendAlways::await_ready" << '\n';
return false;
}
void await_suspend(std::coroutine_handle<>) const noexcept {
std::cout << " MySuspendAlways::await_suspend" << '\n';
}
void await_resume() const noexcept {
std::cout << " MySuspendAlways::await_resume" << '\n';
}
};
struct MySuspendNever { // (2)
bool await_ready() const noexcept {
std::cout << " MySuspendNever::await_ready" << '\n';
return true;
}
void await_suspend(std::coroutine_handle<>) const noexcept {
std::cout << " MySuspendNever::await_suspend" << '\n';
}
void await_resume() const noexcept {
std::cout << " MySuspendNever::await_resume" << '\n';
}
};
struct Job {
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
handle_type coro;
Job(handle_type h): coro(h){}
~Job() {
if ( coro ) coro.destroy();
}
void start() {
coro.resume();
}
struct promise_type {
auto get_return_object() {
return Job{handle_type::from_promise(*this)};
}
MySuspendAlways initial_suspend() { // (3)
std::cout << " Job prepared" << '\n';
return {};
}
MySuspendAlways final_suspend() noexcept { // (4)
std::cout << " Job finished" << '\n';
return {};
}
void return_void() {}
void unhandled_exception() {}
};
};
Job prepareJob() {
co_await MySuspendNever(); // (5)
}
int main() {
std::cout << "Before job" << '\n';
auto job = prepareJob(); // (6)
job.start(); // (7)
std::cout << "After job" << '\n';
}
Zuerst einmal habe ich die vordefinierten Awaitables std::suspend_always
und std::suspend_never
mit den Awaitables MySuspendAlways
(Zeile 1) und MySuspendNever
(Zeile 2) ersetzt. Diese kommen in den Zeilen 3, 4 und 5 zum Einsatz. Sie verhalten sich wie die vordefinierten Awaitables, schreiben aber zusätzlich eine kurze Nachricht. Wegen der Verwendung von std::cout
lassen sich die Methoden await_ready
, await_suspend
und await_resume
nicht als constexpr
erklären.
Der Screenshot zur Programmausführung, der sich direkt auf dem Compiler Explorer nachvollziehen lässt, sollte Licht ins Dunkel bringen.
Die Funktion initial_suspend
(Zeile 3) wird am Anfang der Coroutine und die Funktion final_suspend
an ihrem Ende (Zeile 4) ausgeführt. Der Aufruf prepareJob()
(Zeile 6) stößt das Erzeugen des Coroutinen-Objekts, die Funktion job.start()
ihr Aufwecken und damit ihr vollständiges Ausführen (Zeile 7) an. Konsequenterweise werden den Methoden await_ready
, await_suspend
und await_resume
von MySuspendAlways
ausgeführt. Wenn du das Awaitable, das durch die Methode final_suspend
zurückgegeben wird, nicht aufweckst, wird die Funktion await_resume
nicht ausgeführt. Im Gegensatz dazu ist MySuspendNever
sofort ausführbar, denn await_ready
gibt true
zurück. Damit wird MySuspendNever
nicht pausiert.
Dank der Kommentare sollte der Awaiter-Arbeitsablauf nun vertraut sein. Jetzt ist es an der Zeit, diesen zu variieren.
Wie geht's weiter?
In meinem nächsten Artikel werde ich den Awaiter auf demselben und einem separaten Thread aufwecken.
Fünf Gutscheine für meinen Heise-Academy-Kurs
Für meinen Kurs "Der C++20-Kurs: Concepts, Ranges, Module und Coroutinen" gibt es fünf Gutscheine zu gewinnen. Mich interessiert vor allem die Antwort auf die Frage: Welche C++20-Feature soll ich als Nächstes angreifen? Ein Überblick zu den C++20-Featuren gibt das Inhaltsverzeichnis zu meinen Artikeln für heise Developer.
Anworten bis zum 11. April direkt an Rainer.Grimm@modernescpp.de. ()