コルーチンを使用して別のスレッドで Future を実行する

この投稿で、C++20 の co_return に関する私の投稿を終了します。私は熱心な未来から始め、怠惰な未来を続けました。現在、コルーチンを実装の詳細として使用して、別のスレッドで未来を実行しています。

先に進む前に、強調しておきたいことがあります。 C++20 のコルーチンに関するこのミニシリーズの理由は単純です。コルーチンの複雑なワークフローについて直感を構築する手助けをしたいからです。これが、このミニシリーズのこれまでの出来事です。各投稿は、以前の投稿に基づいています。

co_return :

  • コルーチンを使用したシンプルな Future の実装
  • コルーチンを使用した遅延フューチャー

ここで、別のスレッドでコルーチンを実行したいと考えています。

別のスレッドでの実行

前の例「C++20 のコルーチンを使用した Lazy Futures」のコルーチンは、createFuture のコルーチン本体に入る前に完全に中断されました。 .

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

その理由は、関数 initial_suspend promise の戻り値 std::suspend_always .これは、コルーチンが最初に中断され、したがって別のスレッドで実行できることを意味します

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

}

実行中のスレッドの ID を示すコメントをプログラムにいくつか追加しました。プログラム lazyFutureOnOtherThread.cpp 前のプログラム lazyFuture.cpp とよく似ています 投稿「C++ 20 のコルーチンを使用した Lazy Futures」。メンバー関数の get (1 行目) です。呼び出し std::thread t([this] { coro.resume(); }); (2 行目) 別のスレッドでコルーチンを再開します。

Wandbox オンライン コンパイラでプログラムを試すことができます。

メンバー関数 get についていくつか補足を加えたいと思います . promise が別のスレッドで再開され、coro.promise().result; が返される前に終了することが重要です。 .

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

スレッドに参加する場所 t 呼び出しの後 coro.promise().result を返します 、プログラムは未定義の動作をします。次の関数 get の実装では 、私は std::jthread を使用します . C++20 の std::jthread に関する私の投稿は次のとおりです:「C++20 による改善されたスレッド」。 std::jthread以降 範囲外になると自動的に参加します。これでは遅すぎます。

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

この場合、Promise がメンバー関数 return_value を使用して結果を準備する前に、クライアントが結果を取得する可能性が高くなります。 .さて、result は任意の値を持つため、res も同様です。 .

return 呼び出しの前にスレッドが確実に実行されるようにする方法は他にもあります。
  • std::jthread 独自のスコープを持つ
T get() {
 {
 std::jthread t([this] { coro.resume(); });
 }
 return coro.promise().result;
}

  • Make std::jthread 一時オブジェクト

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

特に、最後の解決策は好きではありません。なぜなら、std::jthread のコンストラクターを呼び出したことを認識するのに数秒かかるからです。 .

今こそ、コルーチンに関する理論を追加する適切な時期です。

promise_type

MyFuture のようなコルーチンが 常に内部型 promise_type を持つ .この名前は必須です。または、std::coroutines_traits  を特化することもできます MyFuture で public promise_type を定義します 初期化。私を含め、すでにこの罠に陥っている人を何人か知っているので、この点を明確に述べました。

これは、私が Windows で陥る別の罠です。

return_void および return_value

プロミスにはメンバー関数 return_voidのいずれかが必要です または return_value.

  • promise には return_void が必要です メンバ関数 if
    • コルーチンに co_return がありません
    • コルーチンには co_return があります 引数なしのステートメント
    • コルーチンには co_return expression があります 式が void. 型のステートメント
  • promise には return_value が必要です co_return を返す場合のメンバー関数 式の型が void であってはならない式ステートメント

return_void なしで void を返すコルーチンの終わりから落ちる メンバー関数は未定義の動作です。興味深いことに、Microsoft ではメンバー関数 return_void が必要ですが、GCC コンパイラでは必要ありません。 コルーチンが常に最終中断ポイントで中断され、したがって最後まで失敗しない場合: std::suspend_always final_suspend() noexcept; 私の見解では、C++20 標準は明確ではなく、常にメンバ関数 void return_void() {} を追加しています。

次は?

新しいキーワード co_return についての私の議論の後 、 co_yield で続行したい . co_yield 無限のデータ ストリームを作成できます。次の投稿でその方法を示します。