Lazy Futures mit Coroutinen

Lazy Futures mit Coroutinen

Basierend auf der Coroutinen-basierten Implementierung eines einfachen Futures in meinem letzten Post „Implementing Simple Futures with Coroutines“, möchte ich heute noch einen großen Schritt weiter gehen. Ich analysiere den Workflow der einfachen Zukunft und mache ihn faul.

Bevor ich Variationen der Zukunft erstelle, sollten Sie ihren Kontrollfluss verstehen. Ich gehe davon aus, dass Sie meinen vorherigen Beitrag kennen:„Implementing Simple Futures with Coroutines“. In diesem Beitrag helfen mir Kommentare, den Kontrollfluss der Coroutine transparent zu machen. Zusätzlich füge ich damit jedem vorgestellten Programm einen Link zu einem Online-Compiler hinzu Sie verwenden und experimentieren direkt mit den Programmen.

Der transparente Kontrollfluss

// 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 return_void() {} 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 Aufruf createFuture (Zeile 1) bewirkt das Erstellen der Instanz von MyFuture (Zeile 2). Vor MyFuture Der Konstruktoraufruf von (Zeile 3) ist abgeschlossen, das Promise promise_type wird erstellt, ausgeführt und zerstört (Zeile 4 - 5). Das Promise verwendet in jedem Schritt seines Kontrollflusses den zu erwartenden std::suspend_never (Zeile 6 und 7) und wird daher niemals ausgesetzt. Um das Ergebnis des Versprechens für später zu speichern fut.get() Anruf (Leitung 8), es muss belegt werden. Weiterhin wird der verwendete std::shared_ptr' s Stellen Sie sicher (Zeile 3 und 10), dass das Programm keinen Speicherverlust verursacht. Als Einheimischer fut in Zeile 12 den Gültigkeitsbereich verlässt und die C++-Laufzeitumgebung ihren Destruktor aufruft.

Sie können das Programm im Compiler Explorer ausprobieren.

Die vorgestellte Coroutine läuft sofort und ist daher eifrig. Außerdem läuft die Coroutine im Thread des Aufrufers.

Machen wir die Zukunft faul.

Eine faule Zukunft

Eine faule Zukunft ist eine Zukunft, die nur läuft, wenn nach dem Wert gefragt wird. Mal sehen, was ich in der vorherigen Koroutine ändern muss, um die Zukunft faul zu machen.

// 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 return_void() {} 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'; }

Lasst uns zuerst das Versprechen studieren. Das Promise setzt immer am Anfang (Zeile 1) und am Ende (Zeile 2) aus. Außerdem die Member-Funktion get_return_object (Zeile 3) erstellt das Rückgabeobjekt, das an den Aufrufer der Coroutine createFuture zurückgegeben wird (Zeile 4). Die Zukunft MyFuture ist interessanter. Es hat einen Handle coro (Zeile 5) zum Versprechen. MyFuture verwendet das Handle, um sein Versprechen zu verwalten. Es nimmt das Promise wieder auf (Zeile 6), fragt das Promise nach dem Ergebnis (Zeile 7) und zerstört es schließlich (Zeile 8). Die Wiederaufnahme der Coroutine ist notwendig, da sie nie automatisch abläuft (Zeile 1). Wenn der Client fut.get() aufruft (Zeile 7) um nach dem Ergebnis der Zukunft zu fragen, setzt es implizit das Versprechen fort (Zeile 6).

Sie können das Programm im Compiler Explorer ausprobieren.

Was passiert, wenn der Klient am Ergebnis der Zukunft nicht interessiert ist und daher die Coroutine nicht wieder aufnimmt? Probieren wir es aus.

int main() {

 std::cout << '\n';

 auto fut = createFuture();
 // auto res = fut.get();
 // std::cout << "res: " << res << '\n';

 std::cout << '\n';

}

Wie Sie vielleicht erraten haben, wird das Promise nie ausgeführt und die Member-Funktionen return_value und final_suspend werden nicht ausgeführt.

Bevor ich diesen Beitrag beende, möchte ich über die lebenslangen Herausforderungen von Coroutinen schreiben.

Lebenslange Herausforderungen von Coroutinen

Eine der Herausforderungen beim Umgang mit Koroutinen besteht darin, die Lebensdauer der Koroutine zu verwalten.

Im ersten Programm eagerFutureWithComments.cpp , habe ich das Coroutine-Ergebnis in einem std::shared_ptr gespeichert . Dies ist kritisch, da die Coroutine eifrig ausgeführt wird.

Im Programm lazyFuture.cpp , der Anruf final_suspend suspendiert immer (Zeile 2):std::suspend_always final_suspend() . Folglich überdauert das Versprechen den Client und ein std::shared_ptr ist nicht mehr notwendig. Rückgabe von std::suspend_never aus der Funktion final_suspend würde in diesem Fall zu undefiniertem Verhalten führen, weil der Client das Versprechen überleben würde. Daher die Lebensdauer von result endet, bevor der Client danach fragt.

Was kommt als nächstes?

Mein letzter Schritt in die Variation der Zukunft fehlt noch. Im nächsten Beitrag setze ich die Coroutine in einem separaten Thread fort.