コルーチンを使用した Lazy Future

前回の投稿「コルーチンを使用したシンプルなフューチャーの実装」でのシンプルなフューチャーのコルーチン ベースの実装に基づいて、今日はさらに大きな一歩を踏み出したいと思います。単純な未来のワークフローを分析し、怠惰にします。

未来のバリエーションを作成する前に、その制御フローを理解する必要があります。私の以前の記事「Implementing Simple Futures with Coroutines.プログラムを直接使用して実験します。

透過的な制御フロー

// 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)

呼び出し createFuture (1 行目) MyFuture のインスタンスを作成します。 (2行目)。 MyFuture より前 のコンストラクター呼び出し (3 行目) が完了し、promise promise_type 作成、実行、および破棄されます (4 ~ 5 行目)。プロミスは、制御フローの各ステップで awaitable std::suspend_never を使用します (6 行目と 7 行目) したがって、一時停止することはありません。 promise の結果を後で保存するには fut.get() 呼び出し (8 行目)、それを割り当てる必要があります。さらに、使用された std::shared_ptr' ■ (3 行目と 10 行目) プログラムがメモリー・リークを引き起こさないことを確認します。ローカルとして、fut 12 行目で範囲外になり、C++ ランタイムがそのデストラクタを呼び出します。

Compiler Explorer でプログラムを試すことができます。

提示されたコルーチンはすぐに実行されるため、熱心です。さらに、コルーチンは呼び出し元のスレッドで実行されます。

未来を怠惰にしよう。

怠惰な未来

遅延フューチャーは、値を要求された場合にのみ実行されるフューチャーです。 future を遅延させるために、前のコルーチンで何を変更する必要があるか見てみましょう。

// 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'; }

まず、約束を学びましょう。 promise は常に最初 (1 行目) と最後 (2 行目) で中断されます。さらに、メンバー関数 get_return_object (3 行目) コルーチンの呼び出し元に返される戻りオブジェクトを作成します createFuture (4 行目)。未来の MyFuture はもっと面白いです。ハンドル coro を持っています (5行目) 約束に。 MyFuture プロミスを管理するためにハンドルを使用します。 promise を再開し (6 行目)、promise に結果を要求し (7 行目)、最後に破棄します (8 行目)。コルーチンは自動的に実行されないため、再開が必要です (1 行目)。クライアントが fut.get() を呼び出したとき (7 行目) 未来の結果を求めるために、暗黙のうちに promise を再開します (6 行目)。

Compiler Explorer でプログラムを試すことができます。

クライアントが将来の結果に関心がなく、したがってコルーチンを再開しない場合はどうなりますか?試してみましょう。

int main() {

 std::cout << '\n';

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

 std::cout << '\n';

}

ご想像のとおり、promise は決して実行されず、メンバー関数は return_value です。 そして final_suspend 実行されません。

この投稿を終了する前に、コルーチンの生涯の課題について書きたいと思います。

コルーチンの生涯課題

コルーチンを扱う際の課題の 1 つは、コルーチンの有効期間を処理することです。

最初のプログラム eagerFutureWithComments.cpp で 、コルーチンの結果を std::shared_ptr に保存しました .コルーチンは熱心に実行されるため、これは重要です。

プログラム内 lazyFuture.cpp 、呼び出し final_suspend 常にサスペンドします (2 行目):std::suspend_always final_suspend() .その結果、promise はクライアントよりも長く存続し、std::shared_ptr もう必要ありません。 std::suspend_never を返す 関数から final_suspend この場合、クライアントが約束よりも長く存続するため、未定義の動作が発生します。したがって、result の寿命は クライアントが要求する前に終了します。

次は?

未来のバリエーションにおける私の最後のステップはまだ欠けています。次の投稿では、別のスレッドでコルーチンを再開します。