1. ホーム
  2. c++

[解決済み] C++11のasync(launch::async)は、高価なスレッド生成を避けるためにスレッドプールを廃止するか?

2022-07-10 09:56:17

質問

この質問と緩やかに関連しています。 C++11でstd::thread pooledはありますか? . 質問は異なりますが、意図は同じです。

質問1:高価なスレッド生成を避けるために、独自の(またはサードパーティの)スレッドプールを使用することはまだ意味があるのでしょうか?

もう1つの質問の結論は、「あなたは std::thread がプールされているかどうかは当てにならない(かもしれないし、そうでないかもしれない)、というのがもう一つの質問の結論でした。しかし std::async(launch::async) はプールされる確率がかなり高いようです。

標準によって強制されているとは思いませんが、IMHO では、すべての優れた 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 はスレッドローカル変数を初期化せずに開始します。多分、これはあなたが望むものではありません。
  • によって生成されたスレッドでは async によって生成されたスレッドでは、スレッドが再利用された可能性があるため、 私にとってはやや不明です。私の理解では、スレッド ローカル変数がリセットされることは保証されませんが、私が間違っているかもしれません。
  • 一方、独自の (固定サイズの) スレッド プールを使用すると、本当に必要な場合は完全に制御することができます。

どのように解決するのですか?

質問1 :

原文がおかしいので、原文から変更しました。私は、次のような印象を持ちました。 Linux のスレッド作成は非常に安かった という印象を持っていたのですが、テストの結果、新しいスレッドでの関数呼び出しと通常のスレッドのオーバーヘッドが莫大であることを突き止めたのです。関数呼び出しを処理するためにスレッドを作成するオーバーヘッドは、普通の関数呼び出しに比べて 10000 倍以上遅いというようなものです。したがって、小さな関数呼び出しを大量に発行する場合は、スレッド プールが良いアイデアかもしれません。

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

IMHO では、Linux カーネルの人々は、スレッド生成を現在よりも安くすることに取り組むべきです。しかし、標準 C++ ライブラリでは、プールを使って launch::async | launch::deferred .

そして、OPが正しいのは ::std::thread を使ってスレッドを起動すると、当然ながらプールからのスレッドを使うのではなく、新しいスレッドを作らざるを得ません。そのため ::std::async(::std::launch::async, ...) が望ましいです。

質問2 :

はい、基本的にこれは「暗黙のうちに」スレッドを立ち上げています。しかし、本当に、何が起こっているかはまだ非常に明白です。だから、暗黙のうちにという言葉は、特に良い言葉だとは思いません。

私はまた、破壊の前にリターンを待つことを強制することが必ずしもエラーであるとは納得していません。というのも、私はあなたが async の呼び出しで、戻ることが期待されていない 'デーモン' スレッドを作成することはできません。そして、それらが戻ることを期待されている場合、例外を無視することはOKではありません。

質問3 :

個人的には、スレッドの起動は明示的であることが好きです。私は、シリアルアクセスを保証できる島々に多くの価値を置いています。そうでなければ、常にどこかでmutexをラップし、それを使用することを忘れないようにしなければならないミュータブルステートで終わってしまいます。

私は「未来」モデルよりもワークキューモデルの方がずっと好きでした。なぜなら、「シリアルの島」が転がっているので、より効果的にミュータブルステートを扱うことができるからです。

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

パフォーマンス テスト

そこで、さまざまな呼び出し方法のパフォーマンスをテストし、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

ワーカスレッドでは、スレッドを立ち上げ、ロックレスキューを使って別のスレッドにリクエストを送り、quot;完了しました"の返信が返ってくるのを待ちました。

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

スレッドを起動するためのオーバーヘッドが膨大であることは明らかです。そして、スレッド間キューを持つワーカスレッドでさえ、VM の Fedora 25 では 20 倍ほど、ネイティブ OS X では約 8 倍の速度が低下しています。

私は、パフォーマンス テストに使用したコードを保持する OSDN チャンバーを作成しました。それはここで見ることができます。 https://osdn.net/users/omnifarious/pf/launch_thread_performance/