Lazy Futures mit Coroutinen in C++20
Beginnend mit der Coroutinen-basierten Implementierung eines einfachen Futures im letzten Artikel möchte ich nun einen Schritt weiter gehen. In diesem Artikel steht die Analyse der einfachen Coroutine an. Dazu soll die Coroutine eine Bedarfsauswertung umsetzen.
- Rainer Grimm
Beginnend mit der Coroutinen-basierten Implementierung eines einfaches Futures im letzten Artikel "C++20: Einfache Futures mit Coroutinen implementieren", möchte ich nun einen Schritt weiter gehen. In diesem Artikel steht die Analyse der einfachen Coroutine an. Dazu soll die Coroutine eine Bedarfsauswertung umsetzen.
Bevor ich eine Variation des Futures vorstelle, sollte man seinen einfachen Programmablauf verstehen. Daher setze ich die Kenntnis des vorherigen Artikels "C++20: Einfache Futures mit Coroutinen implementieren" voraus. Viele Kommentare im Programm sollen helfen, seinen Arbeitsablauf offenzulegen. DarĂĽber hinaus befindet sich bei jedem Programm ein Link zum ausfĂĽhrbaren Programm auf einem Online-Compiler.
Der transparente Arbeitsablauf
// eagerFutureWithComments.cpp
#include <coroutine>
#include <iostream>
#include <memory>
template<typename T>
struct MyFuture {
std::shared_ptr<T> value
MyFuture(std::shared_ptr<T> p): value(p) { // (3)
std::cout << " MyFuture::MyFuture" << '\n';
}
~MyFuture() {
std::cout << " MyFuture::~MyFuture" << '\n';
}
T get() {
std::cout << " MyFuture::get" << '\n';
return *value;
}
struct promise_type { // (4)
std::shared_ptr<T> ptr = std::make_shared<T>(); // (11)
promise_type() {
std::cout << " promise_type::promise_type" << '\n';
}
~promise_type() {
std::cout << " promise_type::~promise_type" << '\n';
}
MyFuture<T> get_return_object() {
std::cout << " promise_type::get_return_object" << '\n';
return ptr;
}
void return_value(T v) {
std::cout << " promise_type::return_value" << '\n';
*ptr = v;
}
std::suspend_never initial_suspend() { // (6)
std::cout << " promise_type::initial_suspend" << '\n';
return {};
}
std::suspend_never final_suspend() noexcept { // (7)
std::cout << " promise_type::final_suspend" << '\n';
return {};
}
void unhandled_exception() {
std::exit(1);
}
}; // (5)
};
MyFuture<int> createFuture() { // (2)
std::cout << "createFuture" << '\n';
co_return 2021;
}
int main() {
std::cout << '\n';
auto fut = createFuture(); // (1)
auto res = fut.get(); // (8)
std::cout << "res: " << res << '\n';
std::cout << '\n';
} // (12)
Der Aururf createFuture
(Zeile 1) stößt das Erzeugen einer Instanz vom Datentyp MyFuture
(Zeile 2) an. Bevor der Konstruktoraufruf von MyFuture
(Zeile 3) vollständig ausgeführt wird, ist der Promise bereits erzeugt, aufgeführt und zerstört worden (Zeilen 4 bis 5). Der Promise wendet in jedem Schritt seines Ablaufs die Awaitable std::suspend_never
(Zeilen 6 und 7) an. Daher wird die Coroutine nicht pausiert. Um das Ergebnis für einen späteren fut.get()
-Aufruf (Zeile 8) zu sichern, ist der Promise zu allokieren. DarĂĽber hinaus sichert der std::shared_ptr
zu (Zeilen 3 und 10), dass das Programm kein Speicherleck verursacht. Als lokale Variable endet der GĂĽltigkeitsbereich von fut
in der Zeile 12. Daher ruft die C++-Laufzeit seinen Destruktor auf.
Das Programm lässt sich direkt mit dem Compiler Explorer ausführen.
Die präsentierte Coroutine wird sofort im Thread des Aufrufers ausgeführt.
Nun werde ich die Coroutine "lazy" ausfĂĽhren.
Ein Lazy Future
Ein Lazy Future wird nur ausgeführt, wenn nach seinem Wert gefragt wird. Mit ein paar kleinen Anpassungen lässt sich die vorherige Coroutine in einen Lazy Future transformieren:
// lazyFuture.cpp
#include <coroutine>
#include <iostream>
#include <memory>
template<typename T>
struct MyFuture {
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
handle_type coro; // (5)
MyFuture(handle_type h): coro(h) {
std::cout << " MyFuture::MyFuture" << '\n';
}
~MyFuture() {
std::cout << " MyFuture::~MyFuture" << '\n';
if ( coro ) coro.destroy(); // (8)
}
T get() {
std::cout << " MyFuture::get" << '\n';
coro.resume(); // (6)
return coro.promise().result;
}
struct promise_type {
T result;
promise_type() {
std::cout << " promise_type::promise_type" << '\n';
}
~promise_type() {
std::cout << " promise_type::~promise_type" << '\n';
}
auto get_return_object() { // (3)
std::cout << " promise_type::get_return_object" << '\n';
return MyFuture{handle_type::from_promise(*this)};
}
void return_value(T v) {
std::cout << " promise_type::return_value" << '\n';
result = v;
}
std::suspend_always initial_suspend() { // (1)
std::cout << " promise_type::initial_suspend" << '\n';
return {};
}
std::suspend_always final_suspend() noexcept { // (2)
std::cout << " promise_type::final_suspend" << '\n';
return {};
}
void unhandled_exception() {
std::exit(1);
}
};
};
MyFuture<int> createFuture() {
std::cout << "createFuture" << '\n';
co_return 2021;
}
int main() {
std::cout << '\n';
auto fut = createFuture(); // (4)
auto res = fut.get(); // (7)
std::cout << "res: " << res << '\n';
std::cout << '\n';
}
Zuerst möchte ich mir den Promise anschauen. Er pausiert immer an seinem Beginn (Zeile 1) und seinem Ende (Zeile 2). Darüber hinaus erzeugt die Funktion get_return_object
(Zeile 3) das Objekt, das an den Aufrufer der Coroutine createFuture
(Zeile 4) zurĂĽckgegeben wird. Der Future MyFuture
ist deutlich interessanter. Er besitzt ein Handle coro
(Zeile 5) auf den Promise. MyFuture
verwendet den Handle, um den Promise zu verwalten. Er weckt den Promise auf (Zeile 6), fragt ihn nach seinem Ergebnis (Zeile 7) und zerstört ihn letztlich (Zeile 8). Das Aufwecken der Coroutine ist notwendig, da sie nicht automatisch ausgeführt wird (Zeile 1). Wenn der Klient fut.get()
(Zeile 7) aufruft, um das Ergebnis zu erhalten, wird die AusfĂĽhrung der Coroutine fortgesetzt (Zeile 6).
Das Programm lässt sich direkt mit dem Compiler Explorer ausführen.
Was passiert, wenn der Klient am Ergebnis nicht interessiert ist und somit die Coroutine nicht aufweckt? Das lässt sich einfach ausprobieren:
int main() {
std::cout << '\n';
auto fut = createFuture();
// auto res = fut.get();
// std::cout << "res: " << res << '\n';
std::cout << '\n';
}
Wie vermutet, werden der Promise und somit die Funktionen return_value
und final_suspend
nicht ausgefĂĽhrt.
Lebenszeitherausforderungen von Coroutinen
Ein der Herausforderungen beim Umgang von Coroutinen ist es, ihre Lebenszeit richtig zu verwalten. Im ersten Programm eagerFuture.cpp
speichert die Coroutine ihr Ergebnis im std::shared_ptr
. Dies ist notwendig, denn die Coroutine wird sofort ausgefĂĽhrt.
Im Programm lazyFuture.cpp
pausiert der Aufruf final_suspend
(Zeile 2) immer: std::suspend_always final_suspend()
. Konsequenterweise lebt der Promise länger als sein Klient, und ein std::shared_ptr
ist nicht notwendig. Wenn die Funktion final_suspend
aber std::suspend_never
verwendet, ist das Verhalten des Programms undefiniert, denn der Klient lebt in diesem Fall länger als der Promise. Damit endet die Gültigkeit von result
, bevor der Klient danach fragt.
Wie geht's weiter?
Mein letzter Schritt in der Variation des Futures fehlt noch. Im nächsten Artikel werde ich die Ausführung der Coroutine auf einem separaten Thread fortsetzen.
C++ Schulungen
Ich freue mich darauf, modernes C++ schulen zu dürfen. Dies sind meine offenen Schulungen im nächsten halben Jahr. Zum jetzigen Zeitpunkt gehe ich davon aus, dass alle Schulungen online stattfinden werden.
- Embedded-Programmierung mit modernem C++: 12. bis 14. April 2022, Termingarantie
- Clean Code mit modernem C++: 22. bis 24. Juni 2021
- C++20: 10. bis 12. August 2021
Mehr Informationen gibt es hier: www.ModernesCpp.de ()