別のスレッドでコルーチンを使用してジョブを自動的に再開する

前回の投稿「コルーチンでジョブを開始する」で、 co_await を適用しました 仕事を始める。この投稿では、ワークフローを改善し、必要に応じてジョブを自動的に再開します。最後のステップとして、別のスレッドでジョブを再開します。

これは、新しいキーワード co_return、co_yield、および co_await に関するミニ シリーズの 7 回目の投稿です。このコルーチンの実践的な紹介を理解するには、以前の投稿をすべて知っておく必要があります:

co_return :

  • コルーチンを使用したシンプルな Future の実装
  • コルーチンを使用した遅延フューチャー
  • コルーチンを使用して別のスレッドで Future を実行する

co_yield:

  • コルーチンによる無限のデータ ストリーム
  • コルーチンを使用した汎用データ ストリーム

co_await:

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

Awaiter の自動再開

前のワークフロー (「コルーチンを使用してジョブを開始する」を参照) で、awaiter ワークフローを詳細に示し、ジョブを明示的に開始しました。

int main() {

 std::cout << "Before job" << '\n';

 auto job = prepareJob();
 job.start();

 std::cout << "After job" << '\n';

}

この明示的な job.start() の呼び出し await_ready のために必要でした Awaitable MySuspendAlways で 常に false を返しました .ここで、await_ready が true を返すことができると仮定しましょう または false ジョブは明示的に開始されません。簡単なリマインダー:await_ready の場合 true を返します 、関数 await_resume 直接呼び出されますが、await_suspend ではありません .

// startJobWithAutomaticResumption.cpp

#include <coroutine>
#include <functional>
#include <iostream>
#include <random>

std::random_device seed;
auto gen = std::bind_front(std::uniform_int_distribution<>(0,1), // (1)
 std::default_random_engine(seed()));

struct MySuspendAlways { // (3)
 bool await_ready() const noexcept { 
 std::cout << " MySuspendAlways::await_ready" << '\n';
 return gen();
 }
 bool await_suspend(std::coroutine_handle<> handle) const noexcept { // (5)
 std::cout << " MySuspendAlways::await_suspend" << '\n';
 handle.resume(); // (6)
 return true;

 }
 void await_resume() const noexcept { // (4)
 std::cout << " MySuspendAlways::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();
 }

 struct promise_type {
 auto get_return_object() { 
 return Job{handle_type::from_promise(*this)};
 }
 MySuspendAlways initial_suspend() { // (2)
 std::cout << " Job prepared" << '\n';
 return {}; 
 }
 std::suspend_always final_suspend() noexcept {
 std::cout << " Job finished" << '\n'; 
 return {}; 
 }
 void return_void() {}
 void unhandled_exception() {}
 
 };
};
 
Job performJob() {
 co_await std::suspend_never();
}
 
int main() {

 std::cout << "Before jobs" << '\n';

 performJob();
 performJob();
 performJob();
 performJob();

 std::cout << "After jobs" << '\n';

}

まず、コルーチンは performJob と呼ばれるようになりました 自動的に実行されます。 gen (行 1) は、数値 0 または 1 の乱数ジェネレーターです。シードで初期化されたデフォルトのランダム エンジンをジョブに使用します。 std::bind_front に感謝 、 std::uniform_int_distribution と一緒にバインドできます これを使用すると、乱数 0 または 1 が返されます。

callable は、関数のように振る舞うものです。これらの名前付き関数だけでなく、関数オブジェクトまたはラムダ式も含まれます。新しい関数 std::bind_frontについて詳しく読む 投稿「C++20 のますます多くのユーティリティ」で。

この例では、awaitable MySuspendAlways を除いて、C++ 標準から定義済みの Awaitables を持つ awaitables を削除しました。 メンバー関数 initial_suspend の戻り値の型として (2行目)。 await_ready (3 行目) ブール値を返します。ブール値が true の場合 、制御フローはメンバー関数 await_resume に直接ジャンプします (4 行目)、false の場合 、コルーチンはすぐに中断されるため、関数 await_suspend 実行します (5 行目)。関数 await_suspend コルーチンへのハンドルを取得し、それを使用してコルーチンを再開します (6 行目)。値 true を返す代わりに 、await_suspend は void を返すこともあります .

次のスクリーンショットは次のとおりです。 await_ready の場合 true を返します 、関数 await_resume await_ready の場合に呼び出されます false を返します 、関数 await_suspend とも呼ばれます。

Compiler Explorer でプログラムを試すことができます。 ここで最後のステップを行い、別のスレッドで awaiter を自動的に再開します。

別のスレッドで Awaiter を自動的に再開する

次のプログラムは、前のプログラムに基づいています。

// startJobWithAutomaticResumptionOnThread.cpp

#include <coroutine>
#include <functional>
#include <iostream>
#include <random>
#include <thread>
#include <vector>

std::random_device seed;
auto gen = std::bind_front(std::uniform_int_distribution<>(0,1), 
 std::default_random_engine(seed()));
 
struct MyAwaitable {
 std::jthread& outerThread;
 bool await_ready() const noexcept { 
 auto res = gen();
 if (res) std::cout << " (executed)" << '\n';
 else std::cout << " (suspended)" << '\n';
 return res; // (6) 
 }
 void await_suspend(std::coroutine_handle<> h) { // (7)
 outerThread = std::jthread([h] { h.resume(); }); // (8)
 }
 void await_resume() {}
};

 
struct Job{
 static inline int JobCounter{1};
 Job() {
 ++JobCounter;
 }
 
 struct promise_type {
 int JobNumber{JobCounter};
 Job get_return_object() { return {}; }
 std::suspend_never initial_suspend() { // (2)
 std::cout << " Job " << JobNumber << " prepared on thread " 
 << std::this_thread::get_id();
 return {}; 
 }
 std::suspend_never final_suspend() noexcept { // (3)
 std::cout << " Job " << JobNumber << " finished on thread " 
 << std::this_thread::get_id() << '\n';
 return {}; 
 }
 void return_void() {}
 void unhandled_exception() { }
 };
};
 
Job performJob(std::jthread& out) {
 co_await MyAwaitable{out}; // (1)
}
 
int main() {

 std::vector<std::jthread> threads(8); // (4)
 for (auto& thr: threads) performJob(thr); // (5)

}

以前のプログラムとの主な違いは、新しい awaitable MyAwaitable です。 、コルーチン performJob で使用 (ライン1)。逆に、コルーチン performJob から返されたコルーチン オブジェクト 簡単です。基本的に、そのメンバー関数 initial_suspend (2 行目) と final_suspend (3 行目) 定義済みの awaitable std::suspend_never. を返します さらに、両方の関数で JobNumber が表示されます 実行されたジョブとそれが実行されるスレッド ID の。スクリーンショットは、すぐに実行されるコルーチンと中断されているコルーチンを示しています。スレッド ID のおかげで、中断されたコルーチンが別のスレッドで再開されたことを確認できます。

Wandbox でプログラムを試すことができます。 プログラムの興味深い制御フローについて説明します。 4 行目は、デフォルトで構築された 8 つのスレッドを作成します。これは、コルーチン performJob (5 行目) 参照によって取得します。さらに、参照は MyAwaitable{out} を作成するための引数になります。 (ライン1)。 res の値に応じて (6 行目)、したがって、関数 await_read の戻り値 y、Awaitable は継続します (res true です ) 実行中または中断中 (res false です )。 MyAwaitableの場合 中断され、関数 await_suspend (7行目)が実行されます。 outerThread の割り当てのおかげで (8行目)、実行中のスレッドになります。実行中のスレッドは、コルーチンの存続期間を超えて存続する必要があります。このため、スレッドには main のスコープがあります。 関数。

次は?

完了:私は C++20 についてほぼ 100 の記事を書きました。次の投稿では、C++20 についていくつかの結論を述べ、C++ に関する「次は何ですか」という質問に答えたいと思います。