web-dev-qa-db-ja.com

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

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

「Parallelism2」または/および「Concurrency2」とはどのような違いがありますか(下の画像をご覧ください)?

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

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

enter image description here

64
Naidu

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

SIMD(単一命令の複数データ)には複数の「実行のスレッド」がありますが、実行状態は1つだけです(複数のデータに対してのみ機能します)。おそらく、異なるデータで1つの「プログラム」を実行するという点で、並列アルゴリズムはこのようなものです。

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

コルーチンには複数の実行状態がありますが、実行スレッドは所有していません。プログラムがあり、プログラムには状態がありますが、実行のスレッドはありません。


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

擬似コードで:

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

Generatorが呼び出され、最初に呼び出されたときに0を返します。その状態は記憶され(コルーチンの実装によってどの程度の状態が変化するか)、次回呼び出したときに中断したところから継続します。したがって、次回は1を返します。その後2。

最後に、ループの終わりに到達し、関数の終わりから落ちます。コルーチンは終了しました。 (ここで何が起こるかは、私たちが話している言語によって異なります。Pythonでは、例外がスローされます)。

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

コルーチンには2種類あります。スタックフルおよびスタックレス。

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

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

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

コルーチンは協調型マルチスレッドに少し似ているため、値を生成するプロセスは「イールド」と呼ばれます。実行ポイントを呼び出し元に明け渡します。

Boostには、スタックフルコルーチンが実装されています。それはあなたに代わって関数を呼び出すことができます。スタックフルコルーチンはより強力ですが、より高価です。


コルーチンには単純なジェネレーター以上のものがあります。コルーチンでコルーチンを待つことができます。これにより、便利な方法でコルーチンを作成できます。

If、ループ、関数呼び出しなどのコルーチンは、別の種類の「構造化goto」であり、特定の有用なパターン(ステートマシンなど)をより自然な方法で表現できます。


C++でのコルーチンの特定の実装は少し興味深いです。

最も基本的なレベルで、C++にいくつかのキーワードを追加します:co_returnco_awaitco_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>に保存してから、generator<int>を介してcurrentの値を返します。

返された整数をループできます。

co_awaitを使用すると、コルーチンを別のコルーチンにスプライスできます。 1つのコルーチンにいて、進行する前に待望の結果(多くの場合コルーチン)が必要な場合は、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_resourceread_linesは、おそらくファイルを開いてそこから行を読み取る非同期コルーチンです。 co_awaitは、load_dataのサスペンドおよび準備完了状態を進捗に関連付けます。

C++コルーチンは、ユーザースペースタイプの上に最小限の言語機能セットとして実装されているため、これよりもはるかに柔軟性があります。ユーザースペースタイプは、co_returnco_awaitおよびco_yieldmeanを効果的に定義します-co_awaitが空のオプションは、空の状態を外側のオプションに自動的に伝播します。

modified_optional<int> add( modified_optional<int> a, modified_optional<int> b ) {
  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;
}
125

コルーチンは、複数のreturnステートメントを持つC関数のようなもので、2回目に呼び出された場合、関数の開始時ではなく、前に実行されたreturnの後の最初の命令で実行を開始します。この実行場所は、非コルーチン関数のスタックに存在するすべての自動変数とともに保存されます。

Microsoftの以前の実験的なコルーチン実装では、コピーされたスタックを使用していたため、深くネストされた関数から戻ることさえできました。しかし、このバージョンはC++委員会によって拒否されました。この実装は、たとえばBoostsファイバーライブラリを使用して取得できます。

9
Lothar

コルーチンは、他のルーチンが完了するのを「待機」し、中断、一時停止、待機、ルーチンを続行するために必要なものを提供できる(C++の)関数であると想定されています。 C++の人々にとって最も興味深い機能は、コルーチンが理想的にはスタックスペースを使用しないことです... C#は既にawaitとyieldでこのようなことを行うことができますが、C++はそれを取得するために再構築する必要があるかもしれません。

並行性は、プログラムが完了するはずのタスクである懸念事項の分離に重点を置いています。この懸念の分離は、多くの手段によって達成される可能性があります...通常、何らかの種類の委任です。並行性の考え方は、多くのプロセスが独立して実行され(懸念の分離)、「リスナー」は、それらの分離された懸念によって生成されたものを、それがどこに行くかに向けることです。これは、何らかの非同期管理に大きく依存しています。アスペクト指向プログラミングなどを含む並行性へのアプローチは多数あります。 C#には「delegate」演算子があり、非常にうまく機能します。

並列処理は並行性のように聞こえますが、実際には物理的な構造であり、コードの一部を実行して結果が返されるさまざまなプロセッサーにコードの一部を向けることができるソフトウェアを使用して、多かれ少なかれ並列に配置された多くのプロセッサーが関与します同期的に。

1
Dr t