ベストアンサー1
今では状況を少し理解できるようになりました (ここでの回答のおかげです!) ので、自分でも少し書いてみようと思いました。
C++11 には、関連はあるものの異なる 2 つの概念があります。非同期計算 (別の場所で呼び出される関数) と並行実行 (スレッド、並行して動作するもの) です。この 2 つは、ある程度直交する概念です。非同期計算は関数呼び出しの異なるフレーバーにすぎず、スレッドは実行コンテキストです。スレッドはそれ自体で便利ですが、この説明では実装の詳細として扱います。
非同期計算には抽象化の階層があります。例として、いくつかの引数を取る関数があるとします。
int foo(double, char, bool);
まず、テンプレートがありますstd::future<T>
は、型 の将来の値を表しますT
。値はメンバー関数 を介して取得できget()
、その結果を待機することでプログラムを効果的に同期します。あるいは、future は をサポートしwait_for()
、これを使用して結果がすでに使用可能かどうかを調べることができます。future は、通常の戻り値の型の非同期の代替として考えてください。この例の関数では、 を想定していますstd::future<int>
。
さて、最高レベルから最低レベルまでの階層を見てみましょう。
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
ここで、のようなものを、制御可能な方法で実装する方法を検討してみましょう。たとえば、関数を別のスレッドで実行するように要求できます。別のスレッドを提供するには、
async
std::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 のラッパーを使用するなど。これは標準ライブラリに含まれている必要があります)。ただし、使用方法の詳細はここでは関係ありません。最終的に参加またはデタッチするようにしてください。重要なのは、関数呼び出しが終了するたびに結果が準備されていることです。join
scoped_thread
std::thread
thr
auto res = fut.get(); // as before
さて、最下層に来ました。パッケージ化されたタスクをどのように実装するのでしょうか?ここで
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_value
set_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();
}