1. ホーム
  2. c++

[解決済み] C++20のコルーチンとは何ですか?

2022-07-17 19:25:53

質問

のコルーチンとは何ですか? c++20 ?

並列処理2」や「並列処理2」とどのように違うのでしょうか?

下の画像はISOCPPのものです。

https://isocpp.org/files/img/wg21-timeline-2017-03.png

どのように解決するのですか?

抽象的なレベルでは、コルーチンは、実行のスレッドを持つという考えから、実行状態を持つという考えを分割します。

SIMD (single instruction multiple data) は複数の実行スレッドを持ちますが、実行状態は1つだけです (複数のデータで動作するだけです)。 おそらく並列アルゴリズムはこれに似ていて、異なるデータに対して1つのプログラム(quot)を実行させます。

スレッドには、複数の実行スレッドと複数の実行状態があります。 複数のプログラム、複数の実行スレッドがあるのです。

コルーチンは複数の実行状態を持ちますが、実行のスレッドを持ちません。 プログラムを持っていて、プログラムは状態を持ちますが、実行スレッドを持ちません。


コルーチンの最も簡単な例は、他の言語からのジェネレータや列挙体です。

擬似コードで

function Generator() {
  for (i = 0 to 100)
    produce i
}

Generator が呼び出され、最初に呼び出されたときには 0 . その状態は記憶され(どの程度の状態かはコルーチンの実装によって異なる)、次に呼ばれたときには、それが去ったところから続けられる。 だから、次に呼ばれたときには1が返される。 次に2が返される。

最後にループの終わりに到達し、関数の終わりから落ちます;コルーチンは終了します。 (ここで何が起こるかは言語によって異なりますが、pythonでは例外を投げます)。

コルーチンはC++にこの機能をもたらします。

コルーチンには、スタックフルとスタックレスの2種類があります。

スタックレスコルーチンは、その状態と実行場所のローカル変数のみを保存します。

スタックフルコルーチンは、スタック全体を格納します(スレッドのようなもの)。

スタックレスコルーチンは非常に軽量にすることができます。 私が読んだ最後の提案は、基本的にあなたの関数をラムダのようなものに書き換えることを含んでいました。すべてのローカル変数はオブジェクトの状態に入り、ラベルはコルーチンが中間結果を生成する場所への/からのジャンプに使用されます。

コルーチンは協調的マルチスレッドに似ているため、値を生成するプロセスをquot;yield"と呼び、実行ポイントを呼び出し側に返すことになります。

Boostにはスタックフルコルーチンの実装があり、yieldするための関数を呼び出すことができます。 スタックフルコルーチンはより強力ですが、より高価でもあります。


コルーチンには単純なジェネレータ以上のものがあります。 コルーチンの中でコルーチンを待ち受けることができるので、便利な方法でコルーチンを構成することができます。

コルーチンは、if、ループ、関数呼び出しと同様に、(ステートマシンのような)特定の有用なパターンをより自然な方法で表現できる、別の種類の"構造化goto"である。


C++におけるコルーチンの具体的な実装は、少し興味深いものです。

最も基本的なレベルでは、C++にいくつかのキーワードを追加しています。 co_return co_await co_yield と、それらと連携するいくつかのライブラリタイプがあります。

関数は、その本体にコルーチンの1つを持つことによって、コルーチンになります。 そのため、宣言上は関数と見分けがつかない。

これら 3 つのキーワードのいずれかが関数本体で使用されると、戻り値の型と引数の標準的な検査が行われ、関数はコルーチンに変換されます。 この検査は、関数が中断されたときに関数の状態をどこに保存するかをコンパイラに知らせます。

最も単純なコルーチンはジェネレータです。

generator<int> get_integers( int start=0, int step=1 ) {
  for (int current=start; true; current+= step)
    co_yield current;
}

co_yield は関数の実行を一時停止し、その状態を generator<int> に格納し、その後 current を通して generator<int> .

返された整数をループすることができます。

co_await は、あるコルーチンを別のコルーチンに継ぎ足すことができます。 もし、あるコルーチンの中にいて、先に進む前に待機しているもの(多くの場合、コルーチン)の結果が必要な場合は co_await を実行する。 もしそれが準備できていれば、すぐに実行し、そうでなければ、待機しているものが準備できるまで中断します。

std::future<std::expected<std::string>> load_data( std::string resource )
{
  auto handle = co_await open_resouce(resource);
  while( auto line = co_await read_line(handle)) {
    if (std::optional<std::string> r = parse_data_from_line( line ))
       co_return *r;
  }
  co_return std::unexpected( resource_lacks_data(resource) );
}

load_data を生成するコルーチンです。 std::future を生成するコルーチンです。

open_resource そして read_line は、おそらくファイルを開いてそこから行を読み出す非同期コルーチンです。 その co_await のサスペンドとレディ状態を結びつけています。 load_data のサスペンドとレディ状態をその進捗に結びつける。

C++のコルーチンは、ユーザースペースの型の上に最小限の言語機能のセットとして実装されたため、これよりもはるかに柔軟です。 ユーザースペースの型は、効果的に co_return co_awaitco_yield というのは -- モナドの省略可能な式を実装するために、これを使う人を見たことがあります。 co_await が自動的に外側のオプショナルに空の状態をプロポゲートするようなモナドオプショナル式を実装しているのを見ました。

modified_optional<int> add( modified_optional<int> a, modified_optional<int> b ) {
  co_return (co_await a) + (co_await b);
}

の代わりに

std::optional<int> add( std::optional<int> a, std::optional<int> b ) {
  if (!a) return std::nullopt;
  if (!b) return std::nullopt;
  return *a + *b;
}