プログラミング
FoundationDB の Flow – C++11 にアクターベースの並行処理をもたらす
FoundationDB's Flow – Bringing Actor-Based Concurrency to C++11 (apple.github.io)
要約
FoundationDB の開発チームは、高性能とスケーラビリティの両立という課題に直面し、C++11 でアクターベースの並行処理を実現する新しい言語「Flow」を開発しました。Flow は、非同期関数(アクター)をコンパイル時に C++11 コードに変換し、コールバックを使用してブロッキングを回避します。これにより、効率的な並行処理、高い生産性、そして信頼性の高いシミュレーションが可能になります。
全文翻訳
Flow エンジニアリングの課題 FoundationDB は、ノードあたりの高性能とスケーラビリティの両方という野心的な目標を持って始まりました。これらの目標を達成するには、FoundationDB コアの開発中に深刻なエンジニアリング課題に直面することを認識していました。Erlang や .NET の Async ライブラリでサポートされているような効率的な非同期通信プロセスを実装する必要がありましたが、同時に C++ の生の速度と I/O 効率も必要でした。さらに、大規模クラスターでの信頼性と耐障害性のためにエンジニアリングを行うには、広範なシミュレーションを実行する必要がありました。これらの課題に対応するため、私たちはいくつかの新しいツールを開発しました。その最初のものが、アクターベースの並行処理を C++11 にもたらす新しいプログラミング言語である Flow です。この機能を追加するために、Flow は並行処理を管理するための新しいキーワードと制御フロープリミティブをいくつか導入しています。Flow は、非同期関数(アクター)を分析し、ブロッキングを回避するためにコールバックを使用する多数の異なるサブ関数としてそれを再書き込みするコンパイラとして実装されています(JavaScript を使用した同様の概念については streamlinejs を参照)。Flow コンパイラの出力は通常の C++11 コードであり、その後、従来のツールを使用してバイナリにコンパイルされます。Flow は、シミュレーションツールにも入力を提供し、物理インターフェイスと障害モードを含むシステム全体の決定論的シミュレーションを実行します。要するに、Flow は、保守可能で拡張可能な方法で C++ 内での効率的な並行処理を可能にし、3 つの主要なエンジニアリング目標すべてを達成します。高性能(ネイティブコードにコンパイルするため)、アクターベースの並行処理(高い生産性開発のため)、シミュレーションサポート(テストのため)。最初の紹介 Flow のアクターは、future と呼ばれるデータ型を使用して互いに非同期メッセージを受信します。アクターが計算を続行するためにデータ値を必要とする場合、他のアクターをブロックせずにそれを待ちます。以下の単純なアクターは非同期加算を実行します。これは、future 整数と通常の整数をオフセットとして受け取り、future 整数を待ち、値とオフセットの合計を返します:ACTOR Future<int> asyncAdd(Future<int> f, int offset) { int value = wait( f ); return value + offset; } Flow の機能 Flow の新しいキーワードと制御フロープリミティブは、コンポーネント間で非同期にメッセージをやり取りする機能に対応しています。以下に簡単な概要を示します。Promise<T> と Future<T> 非同期送信者と受信者を接続するデータ型は、ある C++ 型 T の Promise<T> と Future<T> です。送信者が Promise<T> を保持している場合、それは将来のある時点で Future<T> の保持者に T 型の値を提供する約束を表します。逆に、Future<T> を保持している受信者は、実際に T が必要になる時点まで非同期に計算を続けることができます。Promise と Future は単一プロセス内で使用できますが、分散システムにおける真の強みは、ネットワークを横断できることです。たとえば、あるコンピューターが Promise/Future ペアを作成し、次にネットワーク経由で別のコンピューターに Promise を送信できます。Promise と Future は依然として接続されたままであり、リモートコンピューターによって Promise が満たされると、Future の元の保持者は値が出現するのを確認できます。wait() Future<T> を保持している受信者が計算を続行するために T が必要になった時点で、パラメータとして Future<T> を持つ wait() ステートメントを呼び出します。wait() ステートメントにより、呼び出し元のアクターは Future の値が設定されるまで実行を一時停止し、T 型の値を返します。待機中、他のアクターは実行を続けることができ、単一プロセス内で非同期並行処理を提供します。ACTOR タグが付いた関数のみが wait() を呼び出すことができます。アクターは非同期作業の基本的な単位であり、複雑なメッセージパッシングシステムを作成するために構成できます。アクターを構成することにより、Future は連鎖させることができ、1 つの結果が別の結果に依存するようになります。アクターは Future<T> を返すように宣言され、T はアクターの戻り値がシグナリングにのみ使用される場合は Void になる可能性があります。各アクターは、内部コールバックとサポート関数を持つ C++11 クラスにプリプロセスされます。State state キーワードは、アクター内の複数の wait() ステートメントにわたって表示されるように変数をスコープするために使用されます。状態変数の使用は、以下のアクター例で示されています。PromiseStream<T>、FutureStream<T> コンポーネントが単一のメッセージではなく、非同期メッセージのストリームを処理したい場合、PromiseStream<T> と FutureStream<T> を使用できます。これらの構造は、多重化とメッセージの信頼性の高い配信という 2 つの重要な機能を提供します。また、Flow 設計パターンでも重要な役割を果たします。たとえば、FoundationDB の多くのサーバーは、各リクエストタイプの Promise ストリームの構造としてインターフェイスを公開しています。waitNext() waitNext() は、ストリームに対する wait() の対応物です。プログラムの実行を一時停止し、FutureStream の次の値を待ちます。ストリームに値が準備されている場合、遅延なく実行が継続されます。choose … when choose と when 構造により、アクターは複数の Future を一度に、順序付けられ予測可能な方法で待機できます。例:サーバーインターフェイス 以下は、ネットワーク上で通信する単一サーバーで実行されるアクターです。その機能は、他のアクターからの非同期メッセージに応答してカウントを維持することです。各リクエストタイプに対して choose ステートメントと when を含むループで実装されたインターフェイスをサポートしています。各 when は waitNext() を使用して、ストリーム内の次のリクエストを非同期に待ちます。add および subtract インターフェイスは、state 変数に格納されているカウント自体を変更します。get インターフェイスは、戻りメッセージを送信しやすくするために、単なる int ではなく Promise<int> を受け取ります。同等のコードを直接 C++ で記述する場合、開発者は例外処理を備えた複雑なコールバックセットを実装する必要があり、はるかに多くのエンジニアリング作業が必要になります。Flow は、パフォーマンスの低下なしに、このような非同期調整の実装をはるかに容易にします:ACTOR void serveCountingServerInterface( CountingServerInterface csi) { state int count = 0; while (1) { choose { when (int x = waitNext(csi.addCount.getFuture())){ count += x; } when (int x = waitNext(csi.subtractCount.getFuture())){ count -= x; } when (Promise<int> r = waitNext(csi.getCount.getFuture())){ r.send( count ); // クライアントへ送信 } } } } 注意点 Flow コードは C++ に似ていますが、そうではありません。異なるルールがあり、ファイルはプリプロセスされます。Flow でプログラミングする際には、これを常に念頭に置くことが重要です。IDE や最新のエディター(cquery のような言語サーバーや ycm のような clang ベースの補完エンジン)を引き続き使用したいと考えています。このため、flow を通常の C++ コードとしてコンパイルするためのプリプロセッサ定義を定義するヘッダーファイル actorcompiler.h が flow にあります。CMake は特別なモードもサポートしているため、flow ファイルをプリプロセスしません。これは、cmake に -DOPEN_FOR_IDE=ON を渡すことで使用できます。さらに、ソースディレクトリに特別な compile_commands.json を生成し、IDE やエディターでコンパイルデータベースを探す際にプロジェクトを開くのをサポートします。ただし、一部のプリプロセッサ定義ではすべての問題が解決するわけではありません。Flow でプログラミングする際には、プログラマーが次の点に注意する必要があります。ローカル変数は wait の呼び出しを生き残りません。したがって、これは合法的な Flow コードですが、合法的な C++ コードではありません:ACTOR void foo { int i = 0; wait(someFuture); int i = 2; wait(someOtherFuture) } これが IDE サポートを壊さないようにするには、変数の 2 番目の出現名を変更するか、望ましくない(コードが読みにくくなる可能性がある)場合はスコープを使用できます:ACTOR void foo { { int i = 0; wait(someFuture); } { int i = 2; wait(someOtherFuture) } } ACTOR は内部的にクラスにコンパイルされます。これは、アクター関数内で、this がこのクラスへの有効なポインタであることを意味します。しかし、それらを明示的に(または後述のように暗黙的に)使用すると、IDE サポートが壊れます。代わりに THIS と THIS_ADDR を使用できます。ただし、注意してください。THIS は b