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

tag:C++20]におけるコルーチンとは何ですか?

quot;並列処理2"やquot;並行処理2"とはどのような点で違うのでしょうか?

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

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

[ここに画像の説明を入力]1]。

質問へのコメント (4)
ソリューション

抽象的なレベルでは、コルーチンは実行状態を持つという考え方を、実行スレッドを持つという考え方から切り離したものだ。

SIMD(単一命令複数データ)には複数の実行スレッドがあるが、実行状態は1つしかない(複数のデータで動作するだけ)。 異なるデータに対して1つのプログラムを実行するという点で、並列アルゴリズムはこれに似ていると言える。

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

コルーチンは複数の実行状態を持つが、実行スレッドを持たない。 あなたはプログラムを持っており、プログラムは状態を持っているが、実行スレッドを持っていない。


コルーチンの最も簡単な例は、他の言語のジェネレータや列挙型である。

擬似コードでは

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

ジェネレータが呼ばれ、最初に呼ばれたときは0` を返す。 その状態は記憶され(どの程度の状態かはコルーチンの実装によって異なる)、次に呼び出されたとき、それが中断したところから続く。 つまり、次に呼ばれたときは1を返す。 次に2を返す。

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

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

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

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

スタックフル・コルーチンはスタック全体を保存する(スレッドのようなもの)。

スタックレス・コルーチンは非常に軽量になる。 すべてのローカル変数はオブジェクトのステートに格納され、ラベルはコルーチンが中間結果を生成する場所へのジャンプに使われる。

コルーチンは協調マルチスレッディングのようなものなので、値を生成するプロセスは"yield"と呼ばれます。

Boostにはスタックフルコルーチンの実装がある。 スタックフルコルーチンはより強力だが、より高価でもある。


コルーチンには、単純なジェネレーター以上のものがある。 コルーチンの中でコルーチンを待つことができる。

コルーチンは、if、ループ、関数呼び出しと同様に、(ステート・マシンのような)ある種の有用なパターンをより自然な方法で表現することを可能にする、構造化されたgoto&quotの一種である。


C++におけるコルーチンの具体的な実装はちょっと面白い。

最も基本的なレベルでは、C++にいくつかのキーワードが追加される: co_return co_await co_yield と、それらを扱ういくつかのライブラリ型である。

関数がコルーチンになるには、関数本体にこれらのキーワードを1つ含む必要がある。 つまり、宣言上は関数と区別がつかない。

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

最も単純なコルーチンはジェネレーターである:

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

co_yieldは関数の実行を一時停止し、その状態をgeneratorに格納し、generatorを通してcurrent` の値を返す。

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

一方 co_await を使うと、あるコルーチンを別のコルーチンに接続することができる。 あるコルーチンの中で、処理を進める前に待ち行列(多くの場合コルーチン)の結果が必要な場合、co_awaitする。 もしその結果が準備できていれば、すぐに処理を続行し、もし準備できていなければ、待ち行列が準備できるまで中断する。

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

load_dataは、指定されたリソースがオープンされ、要求されたデータが見つかったところまでパースできたときにstd::future` を生成するコルーチンである。

open_resourceread_lineはファイルを開いてそこから行を読み込む非同期コルーチンであろう。 co_awaitload_dataのサスペンドとレディ状態をそれらの進捗に接続している。

C++のコルーチンは、ユーザー空間の型の上に最小限の言語機能セットとして実装されているので、これよりもはるかに柔軟である。 ユーザー空間の型は、co_return co_awaitco_yield意味 を効果的に定義している -- 私は、空のオプショナルに対する co_await が自動的に空の状態を外側のオプショナルに伝搬するようなモナディックなオプショナル式を実装するために、これを使用している人を見たことがある:

modified_optional add( modified_optional a, modified_optional b ) {
  return (co_await a) + (co_await b);
}

の代わりに

std::optional add( std::optional a, std::optional b ) {
  if (!a) return std::nullopt;
  if (!b) return std::nullopt;
  return *a + *b;
}
解説 (11)

コルーチンは、複数のreturn文を持つC関数のようなもので、2回目に呼び出されたときは、関数の先頭ではなく、前回実行されたreturnの後の最初の命令から実行を開始する。この実行位置は、非コルーチン関数ではスタック上にあるすべての自動変数と一緒に保存されます。

マイクロソフトの以前の実験的なコルーチン実装では、コピーされたスタックを使用していたので、深いネストされた関数からリターンすることもできた。しかし、このバージョンはC++委員会によって却下された。この実装は、例えばBoostsファイバー・ライブラリーで手に入れることができる。

解説 (0)

コルーチンは、(C++では)他のルーチンが完了するのを待ち、中断、一時停止、待機しているルーチンが続行するために必要なものを提供することができる関数であると考えられている。C++の人々にとって最も興味深い機能は、コルーチンが理想的にはスタック空間を取らないことである。

同時実行の考え方は、多くのプロセスが独立して実行され(関心事の分離)、分離された関心事によって生成されたものを、リスナーがどこへでも導くというものだ。 アスペクト指向プログラミングなど、並行性に対するアプローチは数多くある。C#には'delegate'演算子があり、非常にうまく機能する。

並列処理というと並行処理に聞こえるかもしれないが、実際には、多かれ少なかれ並列に配置された多数のプロセッサーと、コードの一部を異なるプロセッサーで実行し、その結果を同期的に受信するソフトウェアが関係する物理的な構造である。

解説 (1)