コルーチンでジョブを開始する

C++20 には、関数からコルーチンを作成するための 3 つの新しいキーワードがあります:co_returnco_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_readyawait_suspend 、および await_resume constexpr として宣言することはできません .

プログラム実行のスクリーンショットは、制御フローを適切に示しており、コンパイラ エクスプローラで直接観察できます。

関数 initial_suspend (3 行目) はコルーチンの先頭で実行され、関数 final_suspend 最後に(4行目)。呼び出し prepareJob() (6 行目) コルーチン オブジェクトの作成をトリガーし、関数 call job.start() その再開と完了 (7 行目)。したがって、メンバー await_readyawait_suspend 、および await_resume MySuspendAlways の 実行されます。メンバ関数 final_suspendが返すコルーチンオブジェクトなどのAwaitableを再開しない場合 、関数 await_resume 処理されません。対照的に、Awaitable の MySuspendNever await_ready のため、関数はすぐに準備完了です true を返します したがって、サスペンドしません。

コメントのおかげで、awaiter ワークフローの基本的な理解が得られます。さあ、それを変える時が来ました。

次は?

次回の投稿では、同じスレッドで Awaiter を自動的に再開し、最後に別のスレッドで再開します。