std::promiseとは何ですか? 質問する

std::promiseとは何ですか? 質問する

私はC++11のstd::threadstd::asyncおよびstd::futureコンポーネント(例えば、この答え)、これらは簡単です。

しかし、それが何なのか、何をするのか、どのような状況で最適に使用できるのか、私にはよくわかりませんstd::promise。標準文書自体には、クラスの概要以外に多くの情報が含まれていません。std::スレッド

std::promiseが必要な状況、そしてそれが最も慣用的な解決策である状況について、簡潔な例を誰か挙げていただけませんか?

ベストアンサー1

今では状況を少し理解できるようになりました (ここでの回答のおかげです!) ので、自分でも少し書いてみようと思いました。


C++11 には、関連はあるものの異なる 2 つの概念があります。非同期計算 (別の場所で呼び出される関数) と並行実行 (スレッド並行して動作するもの) です。この 2 つは、ある程度直交する概念です。非同期計算は関数呼び出しの異なるフレーバーにすぎず、スレッドは実行コンテキストです。スレッドはそれ自体で便利ですが、この説明では実装の詳細として扱います。


非同期計算には抽象化の階層があります。例として、いくつかの引数を取る関数があるとします。

int foo(double, char, bool);

まず、テンプレートがありますstd::future<T>は、型 の将来の値を表しますT。値はメンバー関数 を介して取得できget()、その結果を待機することでプログラムを効果的に同期します。あるいは、future は をサポートしwait_for()、これを使用して結果がすでに使用可能かどうかを調べることができます。future は、通常の戻り値の型の非同期の代替として考えてください。この例の関数では、 を想定していますstd::future<int>

さて、最高レベルから最低レベルまでの階層を見てみましょう。

  1. std::async: 非同期計算を実行する最も便利で簡単な方法はasync、一致する future をすぐに返す関数テンプレートを使用することです。

    auto fut = std::async(foo, 1.5, 'x', false);  // is a std::future<int>
    

    細部についてはほとんど制御できません。特に、関数が同時に実行されるのか、逐次的に実行されるのか、あるいは他の何らかの黒魔術によって実行されるのかさえわかりませんget()。ただし、必要な場合には結果は簡単に得られます。

    auto res = fut.get();  // is an int
    
  2. ここで、のようなものを、制御可能な方法で実装する方法を検討してみましょう。たとえば、関数を別のスレッドで実行するように要求できます。別のスレッドを提供するには、asyncstd::threadクラス。

    次の低いレベルの抽象化では、まさにそれを実行します。std::packaged_taskこれは関数をラップし、関数の戻り値の future を提供するテンプレートですが、オブジェクト自体は呼び出し可能であり、それを呼び出すかどうかはユーザーの判断に委ねられています。次のように設定できます。

    std::packaged_task<int(double, char, bool)> tsk(foo);
    
    auto fut = tsk.get_future();    // is a std::future<int>
    

    タスクを呼び出して完了すると、future が準備完了になります。これは別のスレッドに最適なジョブです。タスクをスレッドに移動させる必要があります。

    std::thread thr(std::move(tsk), 1.5, 'x', false);
    

    スレッドはすぐに実行を開始します。スコープの最後に実行detachすることも、いつでも実行することもできます (たとえば、Anthony Williams のラッパーを使用するなど。これは標準ライブラリに含まれている必要があります)。ただし、使用方法の詳細はここでは関係ありません。最終的に参加またはデタッチするようにしてください。重要なのは、関数呼び出しが終了するたびに結果が準備されていることです。joinscoped_threadstd::threadthr

    auto res = fut.get();  // as before
    
  3. さて、最下層に来ました。パッケージ化されたタスクをどのように実装するのでしょうか?ここでstd::promise約束は未来とのコミュニケーションの基盤となります。主な手順は次のとおりです。

    • 呼び出しスレッドは約束をします。

    • 呼び出しスレッドは、Promise から future を取得します。

    • プロミスは関数の引数とともに別のスレッドに移動されます。

    • 新しいスレッドは関数を実行し、promise を満たします。

    • 元のスレッドが結果を取得します。

    例として、独自の「パッケージ化されたタスク」を以下に示します。

    template <typename> class my_task;
    
    template <typename R, typename ...Args>
    class my_task<R(Args...)>
    {
        std::function<R(Args...)> fn;
        std::promise<R> pr;             // the promise of the result
    public:
        template <typename ...Ts>
        explicit my_task(Ts &&... ts) : fn(std::forward<Ts>(ts)...) { }
    
        template <typename ...Ts>
        void operator()(Ts &&... ts)
        {
            pr.set_value(fn(std::forward<Ts>(ts)...));  // fulfill the promise
        }
    
        std::future<R> get_future() { return pr.get_future(); }
    
        // disable copy, default move
    };
    

    このテンプレートの使用方法は、基本的に と同じですstd::packaged_task。タスク全体を移動すると、Promise も移動することに注意してください。よりアドホックな状況では、Promise オブジェクトを明示的に新しいスレッドに移動し、それをスレッド関数の関数引数にすることもできますが、上記のようなタスク ラッパーの方が柔軟で邪魔にならないソリューションのように思えます。


例外を設ける

Promise は例外と密接に関係しています。Promise のインターフェースだけでは状態を完全に伝えるのに十分ではないため、Promise に対する操作が意味をなさない場合は例外がスローされます。すべての例外は 型でstd::future_error、 から派生しますstd::logic_error。まず、いくつかの制約について説明します。

  • デフォルトで構築されたプロミスは非アクティブです。非アクティブなプロミスは、何の影響もなく終了できます。

  • を介して未来が取得されると、Promise がアクティブになりますget_future()。ただし、取得できる未来は1 つだけです。

  • プロミスは、その未来が消費される場合、その存続期間が終了する前に を介して満たされるかset_value()、 を介して例外が設定されている必要があります。満たされたプロミスは、何の影響もなく終了することができ、未来で使用可能になります。例外のあるプロミスは、未来で を呼び出すと、保存された例外を発生させます。プロミスが値も例外もなしで終了した場合、未来を呼び出すと、「壊れたプロミス」例外が発生します。set_exception()get()get()get()

ここでは、さまざまな例外的な動作を示すための小さなテスト シリーズを紹介します。まず、ハーネスです。

#include <iostream>
#include <future>
#include <exception>
#include <stdexcept>

int test();

int main()
{
    try
    {
        return test();
    }
    catch (std::future_error const & e)
    {
        std::cout << "Future error: " << e.what() << " / " << e.code() << std::endl;
    }
    catch (std::exception const & e)
    {
        std::cout << "Standard exception: " << e.what() << std::endl;
    }
    catch (...)
    {
        std::cout << "Unknown exception." << std::endl;
    }
}

さて、テストに移ります。

ケース1: 非アクティブな約束

int test()
{
    std::promise<int> pr;
    return 0;
}
// fine, no problems

ケース2: アクティブな約束、未使用

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();
    return 0;
}
// fine, no problems; fut.get() would block indefinitely

ケース3: 先物が多すぎる

int test()
{
    std::promise<int> pr;
    auto fut1 = pr.get_future();
    auto fut2 = pr.get_future();  //   Error: "Future already retrieved"
    return 0;
}

ケース4: 約束が果たされる

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();

    {
        std::promise<int> pr2(std::move(pr));
        pr2.set_value(10);
    }

    return fut.get();
}
// Fine, returns "10".

ケース5: 満足しすぎ

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();

    {
        std::promise<int> pr2(std::move(pr));
        pr2.set_value(10);
        pr2.set_value(10);  // Error: "Promise already satisfied"
    }

    return fut.get();
}

またはのいずれかが複数ある場合、同じ例外がスローされます。set_valueset_exception

ケース6: 例外

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();

    {
        std::promise<int> pr2(std::move(pr));
        pr2.set_exception(std::make_exception_ptr(std::runtime_error("Booboo")));
    }

    return fut.get();
}
// throws the runtime_error exception

ケース7: 約束を破る

int test()
{
    std::promise<int> pr;
    auto fut = pr.get_future();

    {
        std::promise<int> pr2(std::move(pr));
    }   // Error: "broken promise"

    return fut.get();
}

おすすめ記事