C++20 には、関数からコルーチンを作成するための 3 つの新しいキーワードがあります:co_return
、 co_yield
、および co_await
. co_await
引数として Awaitable を必要とし、Awaiter ワークフローを開始します。この投稿で、それが何を意味するかをお見せしましょう。
この投稿を理解するには、コルーチンの基本を理解している必要があります。コルーチンに関する私の以前の投稿は、実用的な観点からコルーチンを紹介しています。
co_return
:
- コルーチンを使用したシンプルな Future の実装
- コルーチンを使用した遅延フューチャー
- コルーチンを使用して別のスレッドで Future を実行する
co_yield:
- コルーチンによる無限のデータ ストリーム
- コルーチンを使用した汎用データ ストリーム
Awaitable を実装してそのアプリケーションを示す前に、awaiter ワークフローについて書く必要があります。
Awaiter ワークフロー
まず、簡単なリマインダーがあります。 awaiter ワークフローは、Awaitable: await_ready()
のメンバー関数に基づいています。 , await_suspend()
、および await_resume().
C++20 には 2 つの定義済みの Awaitables std::suspend_always
があります と std::suspend_never
、このミニシリーズでコルーチンに頻繁に使用しました。
std::suspend_always
struct suspend_always { constexpr bool await_ready() const noexcept { return false; } constexpr void await_suspend(std::coroutine_handle<>) const noexcept {} constexpr void await_resume() const noexcept {} };
std::suspend_never
struct suspend_never { constexpr bool await_ready() const noexcept { return true; } constexpr void await_suspend(std::coroutine_handle<>) const noexcept {} constexpr void await_resume() const noexcept {} };
散文の awaiter ワークフローは次のとおりです。
awaitable.await_ready() returns false: // (1) suspend coroutine awaitable.await_suspend(coroutineHandle) returns: // (3) void: // (4) awaitable.await_suspend(coroutineHandle); coroutine keeps suspended return to caller bool: // (5) bool result = awaitable.await_suspend(coroutineHandle); if result: coroutine keep suspended return to caller else: go to resumptionPoint another coroutine handle: // (6) auto anotherCoroutineHandle = awaitable.await_suspend(coroutineHandle); anotherCoroutineHandle.resume(); return to caller resumptionPoint: return awaitable.await_resume(); // (2)
ワークフローは awaitable.await_ready()
の場合にのみ実行されます false
を返します (ライン1)。 true
を返す場合 、コルーチンは準備ができており、呼び出しの結果を返します awaitable.await_resume()
(2行目).
awaitable.await_ready()
と仮定しましょう false
を返します .まず、コルーチンが中断され (3 行目)、すぐに戻り値 awaitable.await_suspend()
が返されます。 評価されます。戻り型は void
にすることができます (4 行目)、ブール値 (5 行目)、または anotherCoroutineHandle.
などの別のコルーチン ハンドル (6 行目) 戻り値の型に応じて、プログラム フローが戻るか、別のコルーチンが実行されます。
理論を適用して、要求に応じて仕事を始めさせてください。
リクエストに応じてジョブを開始する
次の例のコルーチンは、可能な限り単純です。定義済みの Awaitable std::suspend_never()
で待機します .
// startJob.cpp #include <coroutine> #include <iostream> struct Job { struct promise_type; using handle_type = std::coroutine_handle<promise_type>; handle_type coro; Job(handle_type h): coro(h){} ~Job() { if ( coro ) coro.destroy(); } void start() { coro.resume(); // (6) } struct promise_type { auto get_return_object() { return Job{handle_type::from_promise(*this)}; } std::suspend_always initial_suspend() { // (4) std::cout << " Preparing job" << '\n'; return {}; } std::suspend_always final_suspend() noexcept { // (7) std::cout << " Performing job" << '\n'; return {}; } void return_void() {} void unhandled_exception() {} }; }; Job prepareJob() { // (1) co_await std::suspend_never(); // (2) } int main() { std::cout << "Before job" << '\n'; auto job = prepareJob(); // (3) job.start(); // (5) std::cout << "After job" << '\n'; }
コルーチン prepareJob
(1 行目) は、Awaitable が常に中断されるため意味がありません。いいえ!関数 prepareJob
少なくとも co_await
を使用するコルーチン ファクトリです (2 行目) コルーチン オブジェクトを返します。関数呼び出し prepareJob()
3 行目で Job
型のコルーチン オブジェクトを作成します。 .データ型 Job を調べると、promise のメンバー関数が Awaitable std::suspend_always
を返すため、コルーチン オブジェクトがすぐに中断されることがわかります。 (5 行目)。これがまさに関数が job.start
を呼び出す理由です (5 行目) は、コルーチン (6 行目) を再開するために必要です。メンバー関数 final_suspend
() は std::suspend_always
も返します (27行目).
プログラム startJob.cpp
さらなる実験の理想的な出発点です。まず、ワークフローを透明にすることで理解が容易になります。
Transparent Awaiter ワークフロー
以前のプログラムにコメントをいくつか追加しました。
// startJobWithComments.cpp #include <coroutine> #include <iostream> struct MySuspendAlways { // (1) bool await_ready() const noexcept { std::cout << " MySuspendAlways::await_ready" << '\n'; return false; } void await_suspend(std::coroutine_handle<>) const noexcept { std::cout << " MySuspendAlways::await_suspend" << '\n'; } void await_resume() const noexcept { std::cout << " MySuspendAlways::await_resume" << '\n'; } }; struct MySuspendNever { // (2) bool await_ready() const noexcept { std::cout << " MySuspendNever::await_ready" << '\n'; return true; } void await_suspend(std::coroutine_handle<>) const noexcept { std::cout << " MySuspendNever::await_suspend" << '\n'; } void await_resume() const noexcept { std::cout << " MySuspendNever::await_resume" << '\n'; } }; struct Job { struct promise_type; using handle_type = std::coroutine_handle<promise_type>; handle_type coro; Job(handle_type h): coro(h){} ~Job() { if ( coro ) coro.destroy(); } void start() { coro.resume(); } struct promise_type { auto get_return_object() { return Job{handle_type::from_promise(*this)}; } MySuspendAlways initial_suspend() { // (3) std::cout << " Job prepared" << '\n'; return {}; } MySuspendAlways final_suspend() noexcept { // (4) std::cout << " Job finished" << '\n'; return {}; } void return_void() {} void unhandled_exception() {} }; }; Job prepareJob() { co_await MySuspendNever(); // (5) } int main() { std::cout << "Before job" << '\n'; auto job = prepareJob(); // (6) job.start(); // (7) std::cout << "After job" << '\n'; }
まず、定義済みの Awaitables std::suspend_always
を置き換えました そして std::suspend_never
Awaitables MySuspendAlways
付き (1 行目) と MySuspendNever
(2行目)。私はそれらを 3 行目、4 行目、5 行目で使用しています。 std::cout
の使用により 、メンバ関数 await_ready
、 await_suspend
、および await_resume
constexpr
として宣言することはできません .
プログラム実行のスクリーンショットは、制御フローを適切に示しており、コンパイラ エクスプローラで直接観察できます。
関数 initial_suspend
(3 行目) はコルーチンの先頭で実行され、関数 final_suspend
最後に(4行目)。呼び出し prepareJob()
(6 行目) コルーチン オブジェクトの作成をトリガーし、関数 call job.start()
その再開と完了 (7 行目)。したがって、メンバー await_ready
、 await_suspend
、および await_resume
MySuspendAlways
の 実行されます。メンバ関数 final_suspend
が返すコルーチンオブジェクトなどのAwaitableを再開しない場合 、関数 await_resume
処理されません。対照的に、Awaitable の MySuspendNever
await_ready
のため、関数はすぐに準備完了です true
を返します したがって、サスペンドしません。
次は?
次回の投稿では、同じスレッドで Awaiter を自動的に再開し、最後に別のスレッドで再開します。