web-dev-qa-db-ja.com

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

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

質問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_の戻り値を強制的にキャプチャします。そうしないと、デストラクタが実行され、アクションが終了するまでブロックされます。一部では、標準のエラーと見なされています(ハーブサッターなど)。

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

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

別の説明:

私はスレッドプールが他の正当な用途を持っているかもしれないことを知っていますが、この質問では、高価なスレッド作成コストを回避する側面にのみ興味があります

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

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

  • _std::thread_を使用して新しいスレッドを作成すると、初期化されたスレッドローカル変数なしで開始されます。たぶんこれはあなたが望むものではありません。
  • asyncによって生成されたスレッドでは、スレッドが再利用された可能性があるため、それはやや不明瞭です。私の理解では、スレッドローカル変数のリセットは保証されていませんが、間違っている可能性があります。
  • 一方、独自の(固定サイズの)スレッドプールを使用すると、本当に必要な場合に完全に制御できます。
107
Philipp Claßen

質問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)で、OSX 10.13.6の下でApple LLVM version 10.0.0 (clang-1000.10.44.4)を使用すると、次のようになります。

_   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

42
Omnifarious