特別な未来

スレッドの親は、スレッドの子の世話をする必要があります。親は、子供が完了するまで待つか、子供から離れることもできます。しかし、それは本当に新しいことではありません。しかし、それは std::async には当てはまりません。 std::async の大きな魅力は、親が子の世話をしていないことです。

発射して忘れる

std::async は特別な先物を作成します。これらの先物は、関連するプロミスの作業が完了するまでデストラクタで待機します。そのため、作成者は子供の世話をする必要はありません。しかし、それはさらに良くなります。 std::future をファイア アンド フォーゲット ジョブとして実行できます。 std::async によって作成された未来は、その場で実行されます。この場合、std::future fut は変数にバインドされていないため、fut.get() または fut.wait() を呼び出して promise の結果を取得することはできません。

たぶん、私の最後の文は少し混乱しすぎました。そこで私は、普通の未来と火をつけて忘れる未来を比較します。ファイア アンド フォーゲット フューチャーでは、Promise を別のスレッドで実行して、その作業をすぐに開始する必要があります。これは std::launch::async ポリシーによって行われます。非同期関数呼び出し後の起動ポリシーの詳細を読むことができます。

auto fut= std::async([]{return 2011;});
std::cout << fut.get() << std::endl; /// 2011
 
std::async(std::launch::async,[]{std::cout << "fire and forget" << std::endl;}); // fire and forget
 

ファイア アンド フォーゲット フューチャーズにはバッグ チャームが付いています。それらはその場で実行され、作成者が面倒を見ることなく作業パッケージを実行します。簡単な例は、説明されている動作を示しています。

// async.cpp

#include <iostream>
#include <future>

int main() {

 std::cout << std::endl;
 std::async([](){std::cout << "fire and forget" << std::endl;});
 std::cout << "main done " << std::endl;
}
 

取り敢えず、出力です。

その行動に対する評価は高い。高すぎます。

次々と

std::async によって作成される未来は、作業が完了するまでデストラクタで待機します。待機の別の言葉はブロッキングです。 future は、デストラクタでプログラムの進行をブロックします。ファイア アンド フォーゲット フューチャーを使用する場合は、これが明らかになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// blocking.cpp

#include <chrono>
#include <future>
#include <iostream>
#include <thread>

int main(){

 std::cout << std::endl;

 std::async(std::launch::async,[]{
 std::this_thread::sleep_for(std::chrono::seconds(2));
 std::cout << "first thread" << std::endl;
 });
 
 std::async(std::launch::async,[]{
 std::this_thread::sleep_for(std::chrono::seconds(1)); 
 std::cout << "second thread" << std::endl;}
 );
 
 std::cout << "main thread" << std::endl; 

}

プログラムは、独自のスレッドで 2 つの promise を実行します。結果として生じる先物は、ファイア アンド フォーゲット先物です。これらの先物は、関連付けられた約束が完了するまで、デストラクタでブロックされます。その結果、プロミスは、ソース コードで見つかった順序で高い確率で実行されます。これはまさに、プログラムの出力に表示されるものです。

この点をもう一度強調したい。メイン スレッドで 2 つの promise を作成し、それらは別々のスレッドで実行されますが、スレッドは次々と順番に実行されます。そのため、より時間のかかる作業パッケージ (12 行目) を含むスレッドが最初に終了します。うわー、それは残念でした。 3 つのスレッドが同時に実行されるのではなく、各スレッドが次々に実行されます。

重要な問題は、std::async によって作成されたスレッドが、関連付けられた promise が完了するまでデストラクタで待機していることです。これは解決できません。問題は軽減することしかできません。 future を変数にバインドする場合、ブロックは変数がスコープ外になった時点で行われます。これが、次の例で確認できる動作です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// notBlocking.cpp

#include <chrono>
#include <future>
#include <iostream>
#include <thread>

int main(){

 std::cout << std::endl;

 auto first= std::async(std::launch::async,[]{
 std::this_thread::sleep_for(std::chrono::seconds(2));
 std::cout << "first thread" << std::endl;
 });
 
 auto second= std::async(std::launch::async,[]{
 std::this_thread::sleep_for(std::chrono::seconds(1)); 
 std::cout << "second thread" << std::endl;}
 );
 
 std::cout << "main thread" << std::endl; 

}

これで、3 つのスレッドが並列に実行されるため、プログラムの出力は直感と一致します。 future first (12 行目) と second (17 行目) は、main 関数の終了 (24 行目) まで有効です。したがって、デストラクタはおそらくこの時点でブロックします。その結果、作業パッケージが最小のスレッドが最も高速になります。

そんなに悪くない

std::async を使用すると、非常に不自然な先物が作成されることを認めなければなりません。最初は、先物は変数にバインドされていませんでした。第 2 に、get または wait 呼び出しによって promise から結果を取得するために future を使用しませんでした。まさにその状況で、未来がそのデストラクタでブロックする奇妙な動作を観察できます。

これらの投稿の主な理由は、変数にバインドされていないファイア アンド フォーゲット フューチャーを細心の注意を払って処理する必要があることを示すことでした。しかし、この点は、std::packaged_task または std::promise によって作成される先物には当てはまりません。

次は?

あなたはそれを知っていると思います。私は条件変数の大ファンではありません。そこで、条件変数とタスクを比較してスレッドを同期したいと考えています。私が信じているのは、ほとんどの場合、タスクはエラーが発生しにくいため、より良い選択であるということです。それでは、次回の投稿をお楽しみに。 (校正者 Alexey Elymanov )