Ausführen eines Futures in einem separaten Thread mit Coroutinen

Ausführen eines Futures in einem separaten Thread mit Coroutinen

Dieser Beitrag schließt meine Beiträge zu co_return in C++20 ab. Ich begann mit einer eifrigen Zukunft und fuhr mit einer faulen Zukunft fort. Heute führe ich die Zukunft in einem separaten Thread aus, wobei ich Coroutinen als Implementierungsdetail verwende.

Bevor ich fortfahre, möchte ich betonen. Der Grund für diese Miniserie über Coroutinen in C++20 ist einfach:Ich möchte Ihnen helfen, eine Intuition über den komplizierten Workflow von Coroutinen aufzubauen. Das ist bisher in dieser Miniserie passiert. Jeder Beitrag basiert auf den vorherigen.

co_return :

  • Einfache Futures mit Coroutinen implementieren
  • Lazy Futures mit Koroutinen

Jetzt möchte ich die Coroutine in einem separaten Thread ausführen.

Ausführung in einem anderen Thread

Die Coroutine im vorherigen Beispiel „Lazy Futures with Coroutines in C++20“ wurde vollständig angehalten, bevor sie in den Coroutine-Hauptteil von createFuture eintrat .

MyFuture<int> createFuture() {
 std::cout << "createFuture" << '\n';
 co_return 2021;
}

Der Grund war, dass die Funktion initial_suspend des Versprechens gibt std::suspend_always zurück . Das bedeutet, dass die Coroutine zunächst suspendiert ist und somit auf einem separaten Thread ausgeführt werden kann

// lazyFutureOnOtherThread.cpp

#include <coroutine>
#include <iostream>
#include <memory>
#include <thread>

template<typename T>
struct MyFuture {
 struct promise_type;
 using handle_type = std::coroutine_handle<promise_type>; 
 handle_type coro;

 MyFuture(handle_type h): coro(h) {}
 ~MyFuture() {
 if ( coro ) coro.destroy();
 }

 T get() { // (1)
 std::cout << " MyFuture::get: " 
 << "std::this_thread::get_id(): " 
 << std::this_thread::get_id() << '\n';
 
 std::thread t([this] { coro.resume(); }); // (2)
 t.join();
 return coro.promise().result;
 }

 struct promise_type {
 promise_type(){ 
 std::cout << " promise_type::promise_type: " 
 << "std::this_thread::get_id(): " 
 << std::this_thread::get_id() << '\n';
 }
 ~promise_type(){ 
 std::cout << " promise_type::~promise_type: " 
 << "std::this_thread::get_id(): " 
 << std::this_thread::get_id() << '\n';
 }

 T result;
 auto get_return_object() {
 return MyFuture{handle_type::from_promise(*this)};
 }
 void return_value(T v) {
 std::cout << " promise_type::return_value: " 
 << "std::this_thread::get_id(): " 
 << std::this_thread::get_id() << '\n';
 std::cout << v << std::endl;
 result = v;
 }
 std::suspend_always initial_suspend() {
 return {};
 }
 std::suspend_always final_suspend() noexcept {
 std::cout << " promise_type::final_suspend: " 
 << "std::this_thread::get_id(): " 
 << std::this_thread::get_id() << '\n';
 return {};
 }
 void unhandled_exception() {
 std::exit(1);
 }
 };
};

MyFuture<int> createFuture() {
 co_return 2021;
}

int main() {

 std::cout << '\n';

 std::cout << "main: " 
 << "std::this_thread::get_id(): " 
 << std::this_thread::get_id() << '\n';

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

 std::cout << '\n';

}

Ich habe dem Programm einige Kommentare hinzugefügt, die die ID des laufenden Threads zeigen. Das Programm lazyFutureOnOtherThread.cpp ist dem vorherigen Programm lazyFuture.cpp ziemlich ähnlich im Beitrag "Lazy Futures mit Coroutinen in C++20". ist die Member-Funktion get (Zeile 1). Der Aufruf std::thread t([this] { coro.resume(); }); (Zeile 2) setzt die Coroutine auf einem anderen Thread fort.

Sie können das Programm auf dem Wandbox-Online-Compiler ausprobieren.

Ich möchte noch ein paar zusätzliche Bemerkungen zur Member-Funktion get hinzufügen . Es ist entscheidend, dass das Promise, das in einem separaten Thread fortgesetzt wird, beendet wird, bevor es coro.promise().result; zurückgibt .

T get() {
 std::thread t([this] { coro.resume(); });
 t.join();
 return coro.promise().result;
}

Wo ich mich dem Thread anschließe t nach dem Aufruf return coro.promise().result , hätte das Programm ein undefiniertes Verhalten. In der folgenden Implementierung der Funktion get , verwende ich einen std::jthread . Hier ist mein Beitrag über std::jthread in C++20:„An Improved Thread with C++20“. Seit std::jthread schließt sich automatisch an, wenn es den Gültigkeitsbereich verlässt. Das ist zu spät.

T get() { 
std::jthread t([this] { coro.resume(); }); return coro.promise().result; }

In diesem Fall ist es sehr wahrscheinlich, dass der Client sein Ergebnis erhält, bevor das Promise es mithilfe der Member-Funktion return_value vorbereitet . Jetzt result hat einen beliebigen Wert, also auch res .

Es gibt andere Möglichkeiten, um sicherzustellen, dass der Thread vor dem Rückruf beendet ist.
  • std::jthread hat seinen eigenen Geltungsbereich
T get() {
 {
 std::jthread t([this] { coro.resume(); });
 }
 return coro.promise().result;
}

  • Machen Sie std::jthread ein temporäres Objekt

T get() {
std::jthread([this] { coro.resume(); });
return coro.promise().result;
}

Insbesondere die letzte Lösung gefällt mir nicht, da es einige Sekunden dauern kann, bis Sie erkennen, dass ich gerade den Konstruktor von std::jthread aufgerufen habe .

Jetzt ist es an der Zeit, mehr Theorie über Koroutinen hinzuzufügen.

promise_type

Sie fragen sich vielleicht, dass die Coroutine wie MyFuture hat immer den inneren Typ promise_type . Dieser Name ist erforderlich. Alternativ können Sie std::coroutines_traits  spezialisieren auf MyFuture und definieren Sie einen öffentlichen promise_type drin. Ich habe diesen Punkt ausdrücklich erwähnt, weil ich einige Leute kenne, darunter auch mich, die bereits in diese Falle getappt sind.

Hier ist eine weitere Falle, in die ich unter Windows tappe.

return_void und return_value

Das Promise benötigt entweder die Member-Funktion return_void oder return_value.

  • Das Versprechen benötigt einen return_void Mitgliedsfunktion if
    • die Coroutine hat keinen co_return Erklärung.
    • die Coroutine hat einen co_return Aussage ohne Argument.
    • die Coroutine hat einen co_return expression eine Anweisung, bei der der Ausdruck den Typ void. hat
  • Das Versprechen benötigt einen return_value Mitgliedsfunktion, wenn sie co_return zurückgibt expression-Anweisung, wobei expression nicht den Typ void haben darf

Herunterfallen am Ende einer void-zurückkehrenden Coroutine ohne return_void eine Mitgliedsfunktion ist undefiniertes Verhalten. Interessanterweise benötigt der Microsoft-, aber nicht der GCC-Compiler eine Member-Funktion return_void wenn die Coroutine immer an ihrem letzten Unterbrechungspunkt ausgesetzt wird und daher am Ende nicht fehlschlägt: std::suspend_always final_suspend() noexcept; Aus meiner Sicht ist der C++20-Standard nicht klar und ich füge immer eine Member-Funktion void return_void() {} hinzu zu meinem Versprechungstyp.

Was kommt als nächstes?

Nach meiner Diskussion des neuen Schlüsselworts co_return , ich möchte mit co_yield fortfahren . co_yield ermöglicht es Ihnen, unendliche Datenströme zu erstellen. Wie das geht, zeige ich in meinem nächsten Beitrag.