私は今、状況を少しよく理解しています(ここでの回答により、少なからずです!)、私は自分自身の記事を少し追加すると思いました.
C++11 には、関連はあるものの、2 つの異なる概念があります。非同期計算 (別の場所で呼び出される関数) と、同時実行 (スレッド、同時に動作するもの) です。この 2 つは、やや直交する概念です。スレッドが実行コンテキストであるのに対し、非同期計算は関数呼び出しの別のフレーバーです。スレッドはそれ自体が有用ですが、この議論の目的のために、スレッドを実装の詳細として扱います。
非同期計算には抽象化の階層があります。例として、いくつかの引数を取る関数があるとします。
int foo(double, char, bool);
まずstd::future<T>
、 type の将来の値を表す template がありますT
。値はメンバー関数を介して取得できget()
、結果を待機することでプログラムを効果的に同期します。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
。これは、関数をラップし、関数の戻り値に未来を提供するテンプレートですが、オブジェクト自体は呼び出し可能であり、呼び出しはユーザーの裁量に任されています。次のように設定できます。
std::packaged_task<int(double, char, bool)> tsk(foo);
auto fut = tsk.get_future(); // is a std::future<int>
タスクを呼び出して呼び出しが完了すると、未来の準備が整います。これは、別のスレッドの理想的な仕事です。タスクをスレッドに移動することを確認する必要があります。
std::thread thr(std::move(tsk), 1.5, 'x', false);
スレッドはすぐに実行を開始します。それか、スコープの最後か、またはいつでも (たとえば、実際には標準ライブラリにあるはずの Anthony Williams のラッパーを使用して)detach
持つことができます。ただし、使用の詳細はここでは関係ありません。最終的には必ず参加または切り離してください。重要なのは、関数呼び出しが終了するたびに、結果の準備が整っているということです。join
scoped_thread
std::thread
thr
auto res = fut.get(); // as before
これで、最も低いレベルに到達しました。パッケージ化されたタスクをどのように実装しますか? ここで のstd::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 は非アクティブです。非アクティブなプロミスは、結果を伴わずに死ぬ可能性があります。
を介してフューチャを取得すると、Promise がアクティブになりget_future()
ます。ただし、得られる未来は1つだけ!
将来が消費される場合、Promise は、その有効期間が終了する前に、経由で満たされるset_value()
か、経由で例外が設定されている必要があります。set_exception()
満足のいく約束は、結果を伴わずに死ぬget()
可能性があり、将来的に利用可能になります。例外のある promise は、future の呼び出し時に格納された例外を発生さget()
せます。promise が値も例外もなく終了した場合get()
、future を呼び出すと「壊れた promise」例外が発生します。
これらのさまざまな例外的な動作を実証するための小さなテスト シリーズを次に示します。まず、ハーネス:
#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();
}