C ++ 11のasync(launch :: async)は、高価なスレッドの作成を回避するためにスレッドプールを廃止しますか?


117

それはこの質問に緩く関連しています:std :: threadはC ++ 11にプールされていますか?。質問は異なりますが、意図は同じです。

質問1:高価なスレッドの作成を回避するために、独自の(またはサードパーティのライブラリ)スレッドプールを使用することはまだ意味がありますか?

他の質問の結論は、std::threadプールに依存することはできないということでした(そうでない場合もあります)。ただし、std::async(launch::async)プールされる可能性ははるかに高いようです。

それは標準によって強制されているとは思わないが、私見では、スレッドの作成が遅い場合は、すべての優れたC ++ 11実装がスレッドプーリングを使用すると予想します。新しいスレッドを作成するのが安価なプラットフォームでのみ、私は彼らが常に新しいスレッドを生成することを期待します。

質問2:これは私が考えていることですが、それを証明する事実はありません。私はよく間違えられるかもしれません。それは教育を受けた推測ですか?

最後に、ここでは、スレッドの作成が次のように表現できると私が考える方法を最初に示すサンプルコードをいくつか提供しましたasync(launch::async)

例1:

 thread t([]{ f(); });
 // ...
 t.join();

なる

 auto future = async(launch::async, []{ f(); });
 // ...
 future.wait();

例2:スレッドを起動して忘れる

 thread([]{ f(); }).detach();

なる

 // a bit clumsy...
 auto dummy = async(launch::async, []{ f(); });

 // ... but I hope soon it can be simplified to
 async(launch::async, []{ f(); });

質問3:asyncバージョンよりバージョンを優先しthreadますか?


残りはもはや質問の一部ではなく、明確にするためだけです:

戻り値をダミー変数に割り当てる必要があるのはなぜですか?

残念ながら、現在のC ++ 11標準では、の戻り値を強制的に取得します。std::asyncそうでない場合、デストラクタが実行され、アクションが終了するまでブロックされます。これは、規格のエラーと見なされている人もいます(たとえば、Herb Sutterによる)。

cppreference.comからのこの例は、それをうまく示しています。

{
  std::async(std::launch::async, []{ f(); });
  std::async(std::launch::async, []{ g(); });  // does not run until f() completes
}

別の説明:

私は知っているスレッドプールは他の合法的な用途を有することができるが、この質問に私は高価なスレッド作成コストを回避する局面では唯一興味を持っています

特にリソースをさらに制御する必要がある場合は、スレッドプールが非常に役立つ状況がまだあると思います。たとえば、サーバーは、一定の数の要求のみを同時に処理して、高速な応答時間を保証し、メモリ使用量の予測可能性を高めることを決定する場合があります。スレッドプールはここで問題ありません。

スレッドローカル変数も独自のスレッドプールの引数である可能性がありますが、実際に関連があるかどうかはわかりません。

  • std::thread初期化されたスレッドローカル変数なしで、startsで新しいスレッドを作成します。多分これはあなたが望むものではありません。
  • によって生成されasyncたスレッドでは、スレッドが再利用された可能性があるため、私には多少不明確です。私の理解では、スレッドローカル変数がリセットされるとは限りませんが、間違っている可能性があります。
  • 一方、独自の(固定サイズ)スレッドプールを使用すると、本当に必要な場合に完全に制御できます。

8
「しかし、std::async(launch::async)プールされる可能性ははるかに高いようです。」いいえ、それはstd::async(launch::async | launch::deferred)プールされる可能性があると思います。ただでlaunch::asyncタスクは関係なく、他のタスクが実行されているものの新しいスレッドに発売されることになっています。ポリシーlaunch::async | launch::deferredを使用すると、実装はどのポリシーを選択できるようになりますが、より重要なのは、どのポリシーの選択を遅らせるかです。つまり、スレッドプール内のスレッドが使用可能になるまで待機してから、非同期ポリシーを選択できます。
bames53 2013

2
私が知る限り、VC ++のみがでスレッドプールを使用しますstd::async()。それらがスレッドプール内の自明でないthread_localデストラクタをどのようにサポートするかを知りたいと思っています。
bames53 2013

2
@ bames53 gcc 4.7.2に付属するlibstdc ++をステップ実行したところ、起動ポリシーが正確で はないlaunch::async場合、それがlaunch::deferred非同期であるかのように扱われ、決して実行されないことがわかりました。つまり、そのバージョンのlibstdc ++は「選択」します。強制されない限り、常に据え置きを使用する。
doug65536 14

3
@ doug65536 thread_localデストラクタについての私のポイントは、スレッドプールを使用する場合、スレッド出口での破棄は完全に正しくないということでした。タスクが非同期で実行されると、仕様に従って、「新しいスレッドのように」実行されます。つまり、すべての非同期タスクは独自のthread_localオブジェクトを取得します。スレッドプールベースの実装では、同じバッキングスレッドを共有するタスクが独自のthread_localオブジェクトを持っているかのように動作するように、特別な注意を払う必要があります。:このプログラムを検討pastebin.com/9nWUT40h
bames53

2
@ bames53仕様で「新しいスレッドのように」使用することは、私の意見では大きな間違いでした。std::asyncパフォーマンスにとっては美しいものでした。スレッドプールによって自然に支えられた、標準の短期実行タスク実行システムであった可能性もあります。現時点でstd::threadは、スレッド関数が値を返すことができるようにするためにいくつかのがらくたが追加されているだけです。ああ、そして彼らはstd::function完全に仕事をオーバーラップする冗長な「据え置き」機能を追加しました。
doug65536 14

回答:


54

質問1

オリジナルが間違っていたので、これをオリジナルから変更しました。Linuxスレッドの作成は非常に安価であるという印象を受け、テストの結果、新しいスレッドでの関数呼び出しと通常のスレッドでの関数呼び出しのオーバーヘッドは非常に大きいと判断しました。関数呼び出しを処理するスレッドを作成するためのオーバーヘッドは、通常の関数呼び出しよりも10000倍以上遅くなります。したがって、小さな関数呼び出しをたくさん発行している場合は、スレッドプールを使用することをお勧めします。

g ++に同梱されている標準C ++ライブラリにスレッドプールがないことは明らかです。しかし、私は間違いなく彼らのケースを見ることができます。ある種のスレッド間キューを介して呼び出しを押し出さなければならないというオーバーヘッドがあっても、新しいスレッドを開始するよりも安くなるでしょう。そして標準はこれを可能にします。

私見、Linuxカーネルの人々は、スレッドの作成を現在よりも安くするよう努力すべきです。ただし、標準C ++ライブラリでは、プールを使用してを実装することも検討する必要がありますlaunch::async | launch::deferred

そして、OPは正しい::std::threadです。もちろん、スレッドを起動するために使用すると、プールからスレッドを使用する代わりに、新しいスレッドが強制的に作成されます。だから::std::async(::std::launch::async, ...)好ましい。

質問2

はい、基本的にこれは「暗黙的に」スレッドを起動します。しかし、実際には、何が起こっているのかはまだ明らかです。そのため、暗黙のうちに特に良い言葉だとは思いません。

また、破棄する前に返品を待つことを強制することが必ずしもエラーであるとは確信していません。async呼び出しを使用して、返されることが予期されない「デーモン」スレッドを作成する必要があることはわかりません。そして、それらが戻ることが期待される場合、例外を無視することは問題ではありません。

質問3

個人的には、スレッドの起動を明示的にするのが好きです。私はあなたがシリアルアクセスを保証できる島に多くの価値を置きます。そうしないと、ミューテックス状態になり、常にミューテックスをどこかにラップして、使用することを忘れないようにする必要があります。

変更可能な状態をより効果的に処理できるように「シリアルの島」が存在しているため、「未来」モデルよりもはるかに優れたワークキューモデルが気に入りました。

しかし、実際には、それはあなたが何をしているかによります。

性能試験

そこで、さまざまな呼び出し方法のパフォーマンスをテストし、clangバージョン7.0.1およびlibc ++(libstdc ++ではない)でコンパイルされたFedora 29を実行する8コア(AMD Ryzen 7 2700X)システムでこれらの数値を導き出しました。

   Do nothing calls per second:   35365257                                      
        Empty calls per second:   35210682                                      
   New thread calls per second:      62356                                      
 Async launch calls per second:      68869                                      
Worker thread calls per second:     970415                                      

そして、ネイティブ、私のMacBook Pro 15 "(Intel(R)Core(TM)i7-7820HQ CPU @ 2.90GHz)でApple LLVM version 10.0.0 (clang-1000.10.44.4)、OSX 10.13.6の下で、私はこれを手に入れました:

   Do nothing calls per second:   22078079
        Empty calls per second:   21847547
   New thread calls per second:      43326
 Async launch calls per second:      58684
Worker thread calls per second:    2053775

ワーカースレッドでは、スレッドを起動し、ロックレスキューを使用して別のスレッドにリクエストを送信し、「完了」の返信が返されるのを待ちました。

「何もしない」は、テストハーネスのオーバーヘッドをテストするだけです。

スレッドを起動するオーバーヘッドが膨大であることは明らかです。また、スレッド間キューを持つワーカースレッドでも、VMのFedora 25では20倍程度、ネイティブOS Xでは約8倍遅くなります。

パフォーマンステストに使用したコードを保持するBitbucketプロジェクトを作成しました。ここで見つけることができます:https : //bitbucket.org/omnifarious/launch_thread_performance


3
私はワークキューモデルに同意しますが、これには、同時アクセスのすべての使用に適用できるわけではない「パイプライン」モデルが必要です。
Matthieu M.

1
式テンプレート(演算子用)を使用して結果を作成できるように思えますが、関数呼び出しの場合、callメソッドが必要になる思いますが、オーバーロードのため、少し難しいかもしれません。
Matthieu M.

3
「非常に安い」はあなたの経験に関連しています。Linuxスレッドの作成オーバーヘッドは、私の使用にとってかなりのものであると思います。
ジェフ

1
@ジェフ-私はそれがそれよりもずっと安いと思った。実際のコストを見つけるために行ったテストを反映するために、少し前に回答を更新しました。
全面的

4
最初の部分では、脅威を作成するために実行する必要がある量と、関数を呼び出すために実行する必要がある量をいくらか過小評価しています。関数の呼び出しと戻りは、スタックの最上位の数バイトを操作するいくつかのCPU命令です。脅威の作成とは、1。スタックの割り当て、2。システムコールの実行、3。カーネルでのデータ構造の作成とそれらのリンクアップ、途中でロックの把握、4。スケジューラによるスレッドの実行の待機、5。スイッチングスレッドへのコンテキスト。これらの各ステップ自体は、最も複雑な関数呼び出しよりもはるかに時間がかかります。
cmaster-モニカを
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.