std::future 拡張機能

promises と futures の形のタスクは、C++11 では相反する評判を持っています。一方では、スレッドや条件変数よりもはるかに使いやすいです。一方で、彼らには大きな欠点があります。それらは構成できません。 C++20 はこの欠点を克服します。

拡張先物について書く前に、スレッドに対するタスクの利点について少し述べさせてください。

タスクの高度な抽象化

スレッドに対するタスクの主な利点は、プログラマーが何を実行する必要があるかだけを考える必要があり、スレッドの場合などはどのように実行する必要があるかを考える必要がないことです。プログラマーがシステムに実行するジョブを与えると、システムはそのジョブが C++ ランタイムによって可能な限りスマートに実行されるようにします。これは、ジョブが同じプロセスで実行されるか、別のスレッドが開始されることを意味します。これは、ジョブがアイドル状態であるため、別のスレッドがそのジョブを盗んでいることを意味している可能性があります。内部には、ジョブを受け取り、スマートな方法で分散するスレッド プールがあります。それが抽象化ではない場合は?

std::async、std::packaged_task、および std::promise と std::future の形式のタスクに関する投稿をいくつか書きました。詳細はこちらのタスク:しかし、今はタスクの未来です.

拡張先物という名前は非常に簡単に説明できます。まず、std::future のインターフェースが拡張されました。第二に、補償可能な特別な先物を作成するための新しい機能があります。最初のポイントから始めます。

拡張先物

std::future には 3 つの新しいメソッドがあります。

std::future

3 つの新しいメソッドの概要。

  • ラップ解除コンストラクタ ラップされた未来 (future>) の外側の未来をアンラップします。
  • 述語 is_ready 共有状態が利用可能かどうかを返します。
  • 方法 その後 未来に続きを結びつけるもの。

最初はかなり洗練されたものに。未来の状態は、有効または準備ができている可能性があります。

有効と準備

  • 未来は有効 futures に共有状態がある場合 (promise 付き)。 std::future をデフォルトで構築できるため、そうである必要はありません。
  • 未来は準備ができています 共有状態が利用可能な場合。別の言い方をすれば、Promise がすでにその価値を生み出している場合です。

したがって、(valid ==true) は (ready ==true) の要件です。

データ チャネルのエンドポイントとして約束と未来を認識している私のように、有効性と準備ができているという私の精神的なイメージを提示します。私の投稿タスクで写真を見ることができます。

promise へのデータ チャネルが存在する場合、未来は有効です。 Promise がすでにその値をデータ チャネルに投入している場合、未来は準備ができています。

では、その方法に移ります。

then の続き

次に、未来を別の未来に結び付ける力を与えます。ここでは、未来が別の未来に詰め込まれることがよくあります。外側の未来をアンラップするのは、アンラップ コンストラクターの仕事です。

最初のコード スニペットをお見せする前に、提案 n3721 について少しお話ししなければなりません。この記事のほとんどは、「std::future および Releated API の改善」への提案からのものです。それは私の例にも当てはまります。奇妙なことに、res フューチャから結果を取得するために、最終的な get 呼び出しを使用しないことがよくありました。したがって、例に res.get 呼び出しを追加し、結果を変数 myResult に保存しました。さらに、いくつかのタイプミスを修正しました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <future>
using namespace std;
int main() {

 future<int> f1 = async([]() { return 123; });
 future<string> f2 = f1.then([](future<int> f) {
 return to_string(f.get()); // here .get() won’t block
 });

 auto myResult= f2.get();

}

to_string(f.get()) の呼び出し (7 行目) と f2.get() の呼び出し (10 行目) には微妙な違いがあります。最初の呼び出しは非ブロックまたは非同期で、2 番目の呼び出しはブロックまたは非同期です。同期。 f2.get() - 呼び出しは、future-chain の結果が利用可能になるまで待機します。このステートメントは、f1.then(...).then(...).then(...).then(...) などのチェーンにも適用されます。最後の f2.get() 呼び出しがブロックされています。

std::async、std::packaged_task、および std::promise

std::async、std::package_task、および std::promise の拡張機能については、多くを語ることはありません。 3 つすべてが C++20 の拡張先物で返されることを付け加えるだけです。

したがって、先物の構成はよりエキサイティングです。これで、非同期タスクを構成できます。

新しい未来の創造

C++20 は、特別な先物を作成するための 4 つの新しい関数を取得します。これらの関数は、std::make_ready_future、std::make_exceptional_future、std::when_all、および std::when_any です。まずは関数 std::make_ready_future と std::make_exceptional_future へ。

std::make_ready_future および std::make_exceptional_future

どちらの機能も即時な未来を創造します 準備。最初のケースでは、未来には値があります。 2 番目のケースでは例外です。奇妙に思えることは、とても理にかなっています。 C++11 では、準備が整った未来を作成するには promise が必要です。これは、共有状態がすぐに利用できる場合でも必要です。

future<int> compute(int x) {
 if (x < 0) return make_ready_future<int>(-1);
 if (x == 0) return make_ready_future<int>(0);
 future<int> f1 = async([]() { return do_work(x); });
 return f1;
}

したがって、(x> 0) が成り立つ場合、結果は promise を使用してのみ計算する必要があります。短いコメント。両方の関数は、モナドの戻り関数へのペンダントです。拡張先物のこの非常に興味深い側面については、すでに書きました。この投稿で強調したのは、C++20 での関数型プログラミングです。

では、いよいよ未来合成から始めましょう。

std::when_all および std::when_any

両方の機能には多くの共通点があります。

まずは入力から。どちらの関数も、future 範囲または任意の数の future へのイテレータのペアを受け入れます。大きな違いは、イテレータのペアの場合、future は同じ型でなければならないということです。これは、任意の数の Future の場合には当てはまらず、さまざまな型を持つことができ、さらに std::future と std::shared_future を使用することもできます。

関数の出力は、反復子のペアまたは任意の数の先物 (バリアディック テンプレート) が使用されたかどうかによって異なります。どちらの関数も Future を返します。イテレータのペアが使用された場合、std::vector:std::future>> で Future の Future を取得します。可変個引数テンプレートを使用する場合、std::tuple で先物の未来を取得します:std::future, future, ...>>.

それが彼らの共通点でした。すべての入力先物 (when_all)、またはいずれか (when_any) の入力先物の準備ができている場合、両方の関数が返す先物は準備ができています。

次の 2 つの例は、when_all と when_any の使用法を示しています。

when_all

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <future>
using namespace std;

int main() {

 shared_future<int> shared_future1 = async([] { return intResult(125); });
 future<string> future2 = async([]() { return stringResult("hi"); });

 future<tuple<shared_future<int>, future<string>>> all_f = when_all(shared_future1, future2);

 future<int> result = all_f.then([](future<tuple<shared_future<int>,
 future<string>>> f){ return doWork(f.get()); });

 auto myResult= result.get();

}

future all_f (9 行目) は、両方の futures shared_future1 (6 行目) と future2 (Zeile 7) を構成します。 11 行目の先物結果は、基になるすべての先物が準備できている場合に実行されます .この場合、12 行目の将来の all_f が実行されます。結果は将来の利用可能な結果であり、14 行目で使用できます。

when_any

when_any の Future は、11 行目の result で取得できます。result は、どの入力 future が準備できているかの情報を提供します。 when_any_result を使用しない場合は、各 Future に準備ができているかどうかを確認する必要があります。それは面倒です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <future>
#include <vector>

using namespace std;

int main(){

 vector<future<int>> v{ .... };
 auto future_any = when_any(v.begin(), v.end());

 when_any_result<vector<future<int>>> result= future_any.get();

 future<int>& ready_future = result.futures[result.index];

 auto myResult= ready_future.get();

}

future_any は、入力先物のうちの 1 つが準備できた場合に準備が整う先物です。 11 行目の future_any.get() は、将来の結果を返します。 result.futures[result.index] (13 行目) を使用することで、将来の準備が整い、ready_future.get() のおかげでジョブの結果を求めることができます。

次は?

ラッチとバリアは、カウンターを介してスレッドを同期するためにそれをサポートします。次の投稿で紹介します。

2年後、執行者のおかげで先物の未来は大きく変わりました。エグゼキュータの詳細は次のとおりです。