C++20:コルーチンを使用した無限のデータ ストリーム

C++20 のコルーチンに関する私の話は続きます。今日は、コルーチン フレームワークを深く掘り下げて、無限のデータ ストリームを作成します。そのため、準備のために、以前の 2 つの投稿「C++20:コルーチン - 最初の概要」と「C++20:コルーチンの詳細」を読む必要があります。

コルーチンを記述するためのフレームワークは、部分的に実装する必要があり、部分的に上書きできる 20 を超える関数で構成されています。したがって、必要に応じてコルーチンを調整できます。最後に、たとえば、次のような無限データ ストリーム用のジェネレータ Generator を作成できます。

Generator<int> getNext(int start = 0, int step = 1) {
 auto value = start;
 for (int i = 0;; ++i) {
 co_yield value;
 value += step;
 }
}

今、私たちは自分の仕事の運命を知っています。始めましょう。

フレームワーク

コルーチンは、promise オブジェクト、コルーチン ハンドル、コルーチン フレームの 3 つの部分で構成されます。

  • Promise オブジェクト :promise オブジェクトはコルーチン内から操作され、promise オブジェクトを介して結果を配信します。
  • コルーチン ハンドル :コルーチン ハンドルは、外部からコルーチン フレームを再開または破棄する非所有ハンドルです。
  • コルーチン フレーム :コルーチン フレームは内部の、通常はヒープ割り当て状態です。これは、前述の promise オブジェクト、コピーされたコルーチンのパラメーター、一時停止ポイントの表現、現在の一時停止ポイントの前に有効期間が終了するローカル変数、および現在の一時停止ポイントを超える有効期間を持つローカル変数で構成されます。

簡素化されたワークフロー

関数で co_yield、co_await、または co_return を使用すると、関数はコルーチンになり、コンパイラはその本体を次の行と同等のものに変換します。

{
 Promise promise;
 co_await promise.initial_suspend();
 try
 {
 <function body>
 }
 catch (...)
 {
 promise.unhandled_exception();
 }
 FinalSuspend:
 co_await promise.final_suspend();
}

は元の関数本体を表します。コルーチンの簡略化されたワークフローは、次のフェーズで構成されます。

コルーチンが実行を開始します

  • コルーチン フレームを割り当てます
  • すべての関数パラメータをコルーチン フレームにコピーします
  • promise オブジェクトの promise を作成します
  • promise.get_return_object() を呼び出してコルーチン ハンドルを作成し、それをローカル変数に保持します。呼び出しの結果は、コルーチンが最初に中断されたときに呼び出し元に返されます。
  • promise.initial_suspend() と co_await の結果を呼び出します。 promise タイプは通常、積極的に開始されたコルーチンに対して std::suspend_never を返し、遅延的に開始されたコルーチンに対して std::suspend_always を返します。
  • co_await promise.initial_suspend() が再開すると、コルーチンの本体が実行されます

コルーチンが一時停止ポイントに到達

  • コルーチン ハンドル (promise.get_return_object()) が呼び出し元に返され、コルーチンが再開されます

コルーチンが co_return に到達

  • co_return または co_return 式の promise.return_void() を呼び出します。式の型は void です
  • co_return 式の promise.return_value(expression) を呼び出します。ここで、expression は非型 void です。
  • スタックで作成されたすべての変数を破棄します
  • promise.final_suspend() を呼び出し、co_await の結果を呼び出します

コルーチンが破棄されます (co_return、キャッチされない例外、またはコルーチン ハンドルを介して終了することにより)

  • promise オブジェクトの破棄を呼び出します
  • 関数パラメータのデストラクタを呼び出します
  • コルーチン フレームで使用されているメモリを解放します
  • 制御を呼び出し元に戻します

理論を実践してみましょう。

co_yield を使用した無限データ ストリーム

次のプログラムは、無限のデータ ストリームを生成します。コルーチン getNext は、co_yield を使用して、start で開始し、要求に応じて次の値を段階的にインクリメントするデータ ストリームを作成します。

// infiniteDataStream.cpp

#include <coroutine>
#include <memory>
#include <iostream>

template<typename T>
struct Generator {
 
 struct promise_type;
 using handle_type = std::coroutine_handle<promise_type>;
 
 Generator(handle_type h): coro(h) {} // (3)
 handle_type coro;
 
 ~Generator() {
 if ( coro ) coro.destroy();
 }
 Generator(const Generator&) = delete;
 Generator& operator = (const Generator&) = delete;
 Generator(Generator&& oth) noexcept : coro(oth.coro) {
 oth.coro = nullptr;
 }
 Generator& operator = (Generator&& oth) noexcept {
 coro = oth.coro;
 oth.coro = nullptr;
 return *this;
 }
 T getValue() {
 return coro.promise().current_value;
 }
 bool next() { // (5)
 coro.resume();
 return not coro.done();
 }
 struct promise_type {
 promise_type() = default; // (1)
 
 ~promise_type() = default;
 
 auto initial_suspend() { // (4)
 return std::suspend_always{};
 }
 auto final_suspend() {
 return std::suspend_always{};
 }
 auto get_return_object() { // (2)
 return Generator{handle_type::from_promise(*this)};
 }
 auto return_void() {
 return std::suspend_never{};
 }
 
 auto yield_value(const T value) { // (6) 
 current_value = value;
 return std::suspend_always{};
 }
 void unhandled_exception() {
 std::exit(1);
 }
 T current_value;
 };

};

Generator<int> getNext(int start = 0, int step = 1) noexcept {
 auto value = start;
 for (int i = 0;; ++i){
 co_yield value;
 value += step;
 }
}

int main() {
 
 std::cout << std::endl;
 
 std::cout << "getNext():";
 auto gen = getNext();
 for (int i = 0; i <= 10; ++i) {
 gen.next();
 std::cout << " " << gen.getValue(); // (7)
 }
 
 std::cout << "\n\n";
 
 std::cout << "getNext(100, -10):";
 auto gen2 = getNext(100, -10);
 for (int i = 0; i <= 20; ++i) {
 gen2.next();
 std::cout << " " << gen2.getValue();
 }
 
 std::cout << std::endl;
 
}

メイン関数は 2 つのコルーチンを作成します。最初の gen は 0 から 10 までの値を返し、2 番目の gen2 は 100 から -100 までの値を返します。ワークフローに入る前に、Compiler Explorer と GCC 10 のおかげで、ここにプログラムの出力があります。

プログラム∞DataStream.cpp の数字は、ワークフローの最初の繰り返しのステップを表します。

<オール>
  • 約束を作る
  • promise.get_return_object() を呼び出し、結果をローカル変数に保持します
  • ジェネレーターを作成します
  • promise.initial_suspend() を呼び出します。ジェネレーターは遅延しているため、常に中断します。
  • 次の値を要求し、ジェネレーターが消費されているかどうかを返します
  • co_yield 呼び出しによってトリガーされます。その後、次の値が利用可能になります。
  • 次の値を取得します
  • 追加の反復では、ステップ 5 から 7 のみが実行されます。

    コルーチンの基礎となるフレームワークを理解することは非常に困難です。既存のコルーチンをいじって、変更された動作を観察することが、それらを把握する最も簡単な方法かもしれません。無限のデータ ストリームを作成する提示されたコルーチンは、最初の実験の出発点として適しています。コンパイラ エクスプローラで実行可能プログラムへのリンクを使用するだけです。

    次は?

    今日の投稿では、co_yield を使用して無限のデータ ストリームを作成しました。次回は、co_await によるスレッド同期についてです。