コルーチン

コルーチンは、状態を維持しながら実行を中断および再開できる関数です。 C++20 の進化はさらに一歩進んでいます。

この投稿で C++20 の新しいアイデアとして提示したものは、かなり古いものです。コルーチンという用語は、Melvin Conway によって造られました。彼は、1963 年にコンパイラの構築に関する出版物でこれを使用しました。Donald Knuth は、プロシージャをコルーチンの特殊なケースと呼びました。場合によっては、もう少し時間がかかります。

Python のコルーチンは知っていますが、C++20 の新しい概念を理解するのは非常に困難でした。したがって、詳細に入る前に、ここに最初の連絡先があります。

最初の連絡先

新しいキーワード co_await と co_yield により、C++20 は関数の概念を拡張します。

co_await 式のおかげで 式の実行を一時停止および再開することができます。関数 func で co_await 式を使用する場合、関数の結果が利用できない場合、呼び出し auto getResult =func() がブロックされている必要はありません。リソースを消費するブロッキングの代わりに、リソースに優しい待機ができます。

co_yield 式 ジェネレーター関数を記述できるようにします。ジェネレーター関数は、要求に応じて新しい値を返すたびに戻ります。ジェネレーター関数は、値を選択できる一種のデータ ストリームです。データ ストリームは無限にすることができます。したがって、私たちは C++ による遅延評価の中心にいます。

簡単な例

プログラムはできるだけシンプルに。関数 getNumbers は、最初から最後まですべての整数を inc ずつインクリメントして返します。 begin は end よりも小さく、inc は正でなければなりません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// greedyGenerator.cpp

#include <iostream>
#include <vector>

std::vector<int> getNumbers(int begin, int end, int inc= 1){
 
 std::vector<int> numbers;
 for (int i= begin; i < end; i += inc){
 numbers.push_back(i);
 }
 
 return numbers;
 
}

int main(){

 std::cout << std::endl;

 auto numbers= getNumbers(-10, 11);
 
 for (auto n: numbers) std::cout << n << " ";
 
 std::cout << "\n\n";

 for (auto n: getNumbers(0,101,5)) std::cout << n << " ";

 std::cout << "\n\n";

}

もちろん、getNumbers を使用して車輪を再発明しました。C++11 以降、その作業は std::iota で実行できるためです。

完全を期すために、ここに出力があります。

プログラムに関する 2 つの観察が重要です。一方、8 行目のベクトル番号は常にすべての値を取得します。 1000 個の要素を持つベクトルの最初の 5 つの要素だけに関心がある場合でも、それは当てはまります。一方、関数 getNumbers をジェネレーターに変換するのは非常に簡単です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// lazyGenerator.cpp

#include <iostream>
#include <vector>

generator<int> generatorForNumbers(int begin, int inc= 1){
 
 for (int i= begin;; i += inc){
 co_yield i;
 }
 
}

int main(){

 std::cout << std::endl;

 auto numbers= generatorForNumbers(-10);
 
 for (int i= 1; i <= 20; ++i) std::cout << numbers << " ";
 
 std::cout << "\n\n";

 for (auto n: generatorForNumbers(0, 5)) std::cout << n << " ";

 std::cout << "\n\n";

}

ファイル greedyGenerator.cpp の関数 getNumbers は std::vector を返しますが、lazyGenerator.cpp のコルーチン generatorForNumbers はジェネレータを返します。 18 行目のジェネレーター番号または 24 行目の generatorForNumbers(0, 5) は、要求に応じて新しい番号を返します。クエリは、範囲ベースの for ループによってトリガーされます。正確には。コルーチンのクエリは、co_yield i を介して値 i を返し、すぐにその実行を中断します。新しい値が要求された場合、コルーチンはその場所から実行を再開します。

24 行目の getForNumber(0, 5) という式は少し奇妙に見えるかもしれません。これは、ジェネレーターのその場での使用法です。

はっきりと強調したいことがあります。 8 行目の for ループには終了条件がないため、コルーチン generatorForNumbers は無限のデータ ストリームを作成します。 20 行目のように、有限個の値のみを要求する場合は問題ありません。24 行目は当てはまりません。終了条件はありません。

約束通り。コルーチンの詳細はこちら。以下の質問にお答えします:

  • コルーチンの典型的なユースケースは何ですか?
  • コルーチンで使用される概念は何ですか?
  • コルーチンの設計目標は何ですか?
  • 関数はどのようにしてコルーチンになりますか?
  • 2 つの新しいキーワード co_await と co_yield の特徴は何ですか?

詳細

まずは簡単な質問から?

コルーチンの一般的な使用例は?

コルーチンは、イベント駆動型アプリケーションを作成するための自然な方法です。これは、シミュレーション、ゲーム、サーバー、ユーザー インターフェイス、またはアルゴリズムでさえあります。コルーチンは通常、協調的なマルチタスクに使用されます。協調的なマルチタスクの鍵は、各タスクに必要なだけの時間を費やすことです。これは、プリエンプティブ マルチタスクとは対照的です。ここには、各タスクが CPU を取得する時間を決定するスケジューラがあります。

コルーチンにはさまざまなバージョンがあります。

コルーチンで使用される概念は何ですか?

C++20 のコルーチンは非対称で、ファースト クラスで、スタックレスです。

非対称コルーチンのワークフローは呼び出し元に戻ります。これは、対称コルーチンには当てはまりません。対称コルーチンは、そのワークフローを別のコルーチンに委任できます。

コルーチンはデータのように動作するため、ファーストクラス コルーチンはファーストクラス関数に似ています。つまり、それらを引数として使用したり、関数の値を返したり、変数に格納したりできます。

スタックレス コルーチンを使用すると、最上位のコルーチンを一時停止して再開できます。ただし、このコルーチンは別のコルーチンを呼び出すことはできません。

提案 n4402 は、コルーチンの設計目標を説明しています。

コルーチンの設計目標は?

コルーチンは

  • 高度にスケーラブルです (数十億の同時コルーチンまで)。
  • コストが関数呼び出しのオーバーヘッドに匹敵する、非常に効率的な再開操作と一時停止操作
  • オーバーヘッドのない既存施設とのシームレスなやり取り
  • ジェネレーター、ゴルーチン、タスクなど、さまざまな高レベルのセマンティクスを公開するコルーチン ライブラリをライブラリ設計者が開発できるようにする、オープン エンドのコルーチン機構。
  • 例外が禁止または利用できない環境で使用可能

関数がコルーチンになる理由は 4 つあります。

関数はどのようにしてコルーチンになりますか?

を使用すると、関数はコルーチンになります
  • co_return、または
  • co_await、または
  • 共同利回り、または
  • 範囲ベースの for ループ内の co_await 式

この質問に対する答えは、提案 n4628 からのものでした。

最後に、新しいキーワード co_return、co_yield、および co_await について説明します。

co_return、co_yield、co_await

co_return: コルーチンは関数本体から co_return で戻ります。

共同利回り: co_yield のおかげで、ジェネレーターを実装できます。したがって、連続して値をクエリできる無限のデータ ストリームを生成するジェネレータ (lazyGenerator.cpp) を作成できます。ジェネレーター generator generatorForNumbers(int begin, int inc =1) の戻り値の型は、この場合は generator です。 generator は、co_yield i の呼び出しが co_await p.yield_value(i) の呼び出しと同等になるように、特別な promise p を内部的に保持します。 co_yield i は任意に頻繁に呼び出すことができます。呼び出しの直後に、コルーチンの実行が中断されます。

co_await :co_await により、最終的にコルーチンの実行が中断され、再開されます。 co_await exp の式 exp は、いわゆる awaitable 式でなければなりません。 exp は特定のインターフェイスを実装する必要があります。このインターフェイスは 3 つの関数で構成されています e.await_ready、e.await_suspend、および e.await_resume。

co_await の典型的な使用例は、イベントをブロックする方法で待機するサーバーです。

1
2
3
4
5
6
7
Acceptor acceptor{443};
while (true){
 Socket socket= acceptor.accept(); // blocking
 auto request= socket.read(); // blocking
 auto response= handleRequest(request); 
 socket.write(response); // blocking 
}

サーバーは、同じスレッドで各要求に順番に応答するため、非常に単純です。サーバーはポート 443 でリッスンし (1 行目)、その接続を受け入れ (3 行目)、クライアントからの受信データを読み取り (4 行目)、その応答をクライアントに書き込みます (6 行目)。 3 行目、4 行目、6 行目の呼び出しはブロックしています。

co_await のおかげで、ブロッキング呼び出しを一時停止および再開できるようになりました。

1
2
3
4
5
6
7
Acceptor acceptor{443};
while (true){
 Socket socket= co_await acceptor.accept(); 
 auto request= co_await socket.read(); 
 auto response= handleRequest(request); 
 co_await socket.write(responste); 
}

次は?

トランザクション メモリの考え方は、データベース理論のトランザクションに基づいています。トランザクションは、プロパティ A を提供するアクションです トミシティ、C 一貫性、 孤独、そして D 耐久性(ACID)。トランザクショナル メモリは、次の投稿のトピックになります。