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


104

コルーチンとは

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

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

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

ここに画像の説明を入力してください


3
コルーチンの概念は、並列処理並行処理とどのように異なりますか?」- en.wikipedia.org/wiki/Coroutine
ベンフォークト


3
。コルーチンに非常に良いと簡単にフォローイントロはジェームズMcNellisのプレゼンテーション「C ++コルーチン入門」(Cppcon2016)である
philsumuru

2
最後に、「C ++のコルーチンは、他の言語のコルーチンおよび再開可能な関数の実装とどのように異なるのか」についても説明するとよいでしょう。(上記のリンクされたウィキペディアの記事は言語にとらわれないため、取り上げられていません)
Ben Voigt '19

1
他に誰がこの「C ++ 20の隔離」を読んだのですか?
Sahib Yar

回答:


198

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

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

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

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


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

擬似コードでは:

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

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

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

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

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

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

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

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

コルーチンは協調マルチスレッドのように少しなので、値を生成するプロセスは「yield」と呼ばれます。実行ポイントを呼び出し元に戻します。

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


コルーチンには、単純なジェネレーターだけではありません。コルーチンでコルーチンを待つことができるため、コルーチンを便利な方法で作成できます。

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


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

最も基本的なレベルでは、いくつかのキーワードをC ++に追加co_return co_await co_yieldします。

関数は、それらの1つを本体に入れることによってコルーチンになります。したがって、それらの宣言から、関数と区別がつきません。

これら3つのキーワードの1つが関数本体で使用されると、戻り値の型と引数の標準的な必須の検査が行われ、関数がコルーチンに変換されます。この検査は、関数が中断されたときに関数の状態を格納する場所をコンパイラーに指示します。

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

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その間、1つのコルーチンを別のコルーチンにスプライスできます。あなたが1つのコルーチンにいて、進行する前に待てばよいもの(多くの場合コルーチン)の結果が必要な場合はco_await、それを実行します。準備ができている場合は、すぐに続行します。そうでない場合は、待機しているawaitableの準備ができるまで中断します。

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_datastd::future名前付きリソースが開かれたときにを生成するコルーチンであり、要求されたデータが見つかったポイントまで解析できます。

open_resourceread_linesはおそらくファイルを開き、そこから行を読み取る非同期コルーチンです。co_await停止と準備状態を接続load_dataの進捗状況に。

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

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;
}

26
これは、私が今まで読んだコルーチンが何であるかについての最も明確な説明の1つです。それらをSIMDおよび従来のスレッドと比較し、区別することは優れたアイデアでした。
18

2
追加オプションの例がわかりません。std :: optional <int>は待機可能なオブジェクトではありません。
Jive Dadson、2018

1
@mordはい、1つの要素を返すことになっています。磨く必要があるかもしれません。複数のラインが必要な場合は、異なる制御フローが必要です。
Yakk-Adam Nevraumont

1
@lf申し訳ありませんが、そうでした;;
Yakk-Adam Nevraumont

1
そのような単純な関数の@LFは、おそらく違いはありません。しかし、私が一般的に見ている違いは、静的関数が毎回最初から実行を開始するのに対し、コルーチンはその本体の入り口/出口(実行)ポイントを記憶していることです。「ローカル」データの場所は関係ないと思います。
avp

21

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

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


1

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

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

並列処理は並行性のように聞こえ、関与する可能性がありますが、実際には、コードの一部を実行する場所の異なるプロセッサにコードを送り、結果を受け取ることができるソフトウェアと並列に配置された多くのプロセッサを含む物理的な構造です同期的に。


9
並行性と懸念の分離はまったく無関係です。コルーチンが中断ルーチンのための情報を提供していない、彼らはある再開可能ルーチン。
Ben Voigt 2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.