回り道の終わり:統一された未来

エグゼキューターへの最後の投稿の後、ようやく統一先物について書くことができます。未来の長い過去についての記事を書き、C++ コア ガイドラインからの回り道を終わらせます。

promise と future の長い過去は C++11 で始まりました。

C++11:標準化された先物

promise と futures の形のタスクは、C++11 ではあいまいな評判を持っています。一方では、スレッドや条件変数よりもはるかに使いやすいです。一方で、彼らには重大な欠陥があります。それらは合成できません。 C++20/23 は、この欠点を克服する可能性があります。 std::async、std::packaged_task、または std::promise と std::future の形式でタスクについて書きました。詳細については、タスクへの投稿をお読みください。 C++20/23 では、拡張された先物が得られる可能性があります。

同時実行 TS:拡張された先物

先物の問題のため、ISO/IEC TS 19571:2016 は先物に拡張機能を追加しました。鳥瞰的な観点から、それらは構図をサポートします。拡張された未来は、その前任者 (then) が準備できたとき、その前任者のいずれかが準備できたとき、またはすべての前任者が準備できたときに準備が整います。名前空間 std::experimental で利用できます。興味がある場合は、詳細を以下に示します:std::future 拡張機能。

これは、長い議論の終点ではありませんでした。エグゼキュータのルネサンスにより、未来の未来が変わりました。

ユニファイド フューチャーズ

論文 P0701r1:Back to the std2::future パート II では、既存の Future と拡張された Future の欠点の概要を説明しています。

既存の先物のデメリット

future/promise は std::thread 実行エージェントに結合されるべきではありません

C++11 には、std::thread という 1 つのエグゼキューターしかありませんでした。したがって、future と std::thread は切り離すことができませんでした。これは、C++17 と STL の並列アルゴリズムで変更されました。これは、将来の構成に使用できる新しいエグゼキューターによってさらに変化します。たとえば、future は別のスレッド、スレッド プール、または単に順次に実行される可能性があります。

.then 継続が呼び出される場所

次の例のような単純な継続があると想像してください。

future f1 = async([]{ return 123; });
future f2 = f1.then([](future f) {
 return to_string(f.get());
});


問題は、継続をどこで実行するかです。現在、いくつかの可能性があります:

<オール>
  • 消費者側 :コンシューマ実行エージェントは常に継続を実行します。
  • プロデューサー側 :プロデューサー実行エージェントは常に継続を実行します。
  • Inline_executor セマンティクス: 継続が設定されたときに共有状態が準備完了の場合、コンシューマ スレッドは継続を実行します。継続が設定されたときに共有状態の準備ができていない場合、プロデューサー スレッドは継続を実行します。
  • thread_executor セマンティクス :新しい std::thread が継続を実行します。
  • 特に、最初の 2 つの可能性には重大な欠点があります。つまり、ブロックされます。最初のケースでは、コンシューマーはプロデューサーの準備が整うまでブロックします。 2 番目のケースでは、プロデューサーはコンシューマーの準備が整うまでブロックします。

    ドキュメント P0701r184 からのエグゼキューター伝播のいくつかの優れた使用例を次に示します。

    auto i = std::async(thread_pool, f).then(g).then(h);
    // f, g and h are executed on thread_pool.
    
    auto i = std::async(thread_pool, f).then(g, gpu).then(h);
    // f is executed on thread_pool, g and h are executed on gpu.
    
    auto i = std::async(inline_executor, f).then(g).then(h);
    // h(g(f())) are invoked in the calling execution agent.
    

    先物を .then Continuations に渡すのは扱いにくい

    未来は値ではなく継続に渡されるため、構文は非常に複雑です。
    まず、正しいが冗長なバージョンです。

    std::future f1 = std::async([]() { return 123; });
    std::future f2 = f1.then([](std::future f) {
     return std::to_string(f.get());
    });
    

    ここで、std::future で to_string がオーバーロードされているため、値を渡すことができると仮定します。

    std::future f1 = std::async([]() { return 123; });
    std::future f2 = f1.then(std::to_string);
    

    when_all と when_any の戻り値の型は扱いにくい

    ポスト std::future Extensions は、when_all と when_any の非常に複雑な使い方を示しています。

    先物での条件付きブロック Destructor Must Go

    ファイア アンド フォーゲット フューチャーは非常に有望に見えますが、重大な欠点があります。 std::async によって作成された future は、promise が完了するまでデストラクタを待機します。同時実行と思われるものは順次実行されます。ドキュメント P0701r1 によると、これは受け入れられず、エラーが発生しやすくなっています。

    特別な未来の投稿で、火と忘れの先物の独特な動作について説明します。

    即時値と将来値は簡単に構成できる必要があります

    C++11 では、future を作成する便利な方法はありません。約束から始めなければなりません。

    std::promise<std::string> p;
    std::future<std::string> fut = p.get_future();
    p.set_value("hello");
    

    これは、同時実行 TS v1 の関数 std::make_ready_future で変更される可能性があります。

    std::future<std::string> fut = make_ready_future("hello");
    

    未来の引数と非未来の引数を使用すると、仕事がさらに快適になります。

    bool f(std::string, double, int);
    
    std::future<std::string> a = /* ... */;
    std::future<int> c = /* ... */;
    
    std::future<bool> d1 = when_all(a, make_ready_future(3.14), c).then(f);
    // f(a.get(), 3.14, c.get())
    
    std::future<bool> d2 = when_all(a, 3.14, c).then(f);
    // f(a.get(), 3.14, c.get())
    

    同時実行性 TS では、構文形式 d1 も構文形式 d2 も使用できません。

    5 つの新しい概念

    統一先物に対する提案 1054R085 には、先物と約束に関する 5 つの新しい概念があります。

    • 未来の継続 、future の値または例外を引数として呼び出される呼び出し可能なオブジェクト。
    • セミフューチャー ContinuableFuture を生成する操作であるエグゼキュータにバインドできます (f =sf.via(exec))。
    • ContinuableFutureSemiFuture を改良します インスタンスは 1 つの FutureContinuation を持つことができます f.then(c)) にアタッチされた c は、future f の準備ができたときに、future に関連付けられたエグゼキュータで実行されます。
    • SharedFutureContinuableFuture を改良します インスタンスは複数の FutureContinuations を持つことができます
    • 約束 、それぞれが未来に関連付けられており、値または例外のいずれかで未来を準備します。

    このペーパーは、これらの新しい概念の宣言も提供します:

    template <typename T>
    struct FutureContinuation
    {
     // At least one of these two overloads exists:
     auto operator()(T value);
     auto operator()(exception_arg_t, exception_ptr exception);
    };
    
    template <typename T>
    struct SemiFuture
    {
     template <typename Executor>
     ContinuableFuture<Executor, T> via(Executor&& exec) &&;
    };
    
    template <typename Executor, typename T>
    struct ContinuableFuture
    {
     template <typename RExecutor>
     ContinuableFuture<RExecutor, T> via(RExecutor&& exec) &&;
    
     template <typename Continuation>
     ContinuableFuture<Executor, auto> then(Continuation&& c) &&;
    };
    
    template <typename Executor, typename T>
    struct SharedFuture
    {
     template <typename RExecutor>
     ContinuableFuture<RExecutor, auto> via(RExecutor&& exec);
    
     template <typename Continuation>
     SharedFuture<Executor, auto> then(Continuation&& c);
    };
    
    template <typename T>
    struct Promise
    {
     void set_value(T value) &&;
    
     template <typename Error>
     void set_exception(Error exception) &&;
     bool valid() const;
    };
    

    概念の宣言に基づいて、いくつかの所見があります:

    • FutureContinuation は、値または例外で呼び出すことができます。
    • すべての先物 (SemiFuture、ContinuableFuture、および SharedFuture) には、Executor を除き、ContinuableFuture を返すメソッド via があります。 via を使用すると、別のエグゼキューターを使用して、ある Future 型から別の Future 型に変換できます。
    • 継続のための then メソッドを持つのは、ContinuableFuture または SharedFuture だけです。 then メソッドは FutureContinuation を受け取り、ContinuableFuture を返します。
    • Promise は値または例外を設定できます。

    今後の作業

    提案 1054R086 はいくつかの疑問を残しました。

    • 先物とプロミスの進行保証
    • 非同時実行エージェントからの先物とプロミスを使用するための同期に関する要件
    • 標準化された std::future および std::promise との相互運用性
    • future とより高度な形式の両方の、将来のアンラップ。具体的なケースでは、未来のアンラッピングは外側の未来を削除する必要があります。
    • when_all、when_any、または when_n の実装
    • std::async との相互運用性

    将来それらについて書くことを約束します。

    次は?

    次回の投稿では、C++ コア ガイドラインをたどる旅を続けます。今回はロックフリープログラミングについて書きます。