この記事では、条件変数を調べます...
条件変数
前述の同期メソッドに加えて、C ++ 11は、別のスレッドからの通知が受信されるか、
<condition_variable>
ヘッダーで使用できる条件変数の実装は2つあります。
- condition_variable :
std::unique_lock
実行する前にスレッドが待機する必要があります - condition_variable_any :ブロックできる任意のタイプで機能するより一般的な実装。 この実装は、使用するのに(リソースとパフォーマンスの点で)より高価になる可能性があるため、提供する追加機能が必要な場合にのみ使用してください。
条件変数の仕組みを説明します。
- 何らかの条件が真になるのを待機するスレッドが少なくとも1つ必要です。 待機スレッドは最初に
unique_lock
実行する必要があります。 このロックはwait()
メソッドに渡されます。このメソッドは、mutexを解放し、条件変数からの信号を受信するまでストリームを一時停止します。 これが発生すると、スレッドが起動し、lock
が再度実行されます。 - 条件が真になったことを示す少なくとも1つのスレッドが必要です。 シグナルはnotify_one()を使用して送信でき、待機スレッドの1つ(いずれか)がロック解除されるか、またはnotify_all()がすべての待機スレッドをロック解除します。
- マルチプロセッサシステムで予測可能な覚醒状態を作成する際のいくつかの困難により、スプリアスウェイクアップが発生する可能性があります。 これは、誰も条件変数を通知していない場合でも、スレッドを起動できることを意味します。 したがって、フローが覚醒した後、覚醒条件が真であるかどうかを確認する必要があります。 なぜなら 誤った覚醒は何度も発生する可能性があるため、このようなチェックはサイクルで編成する必要があります。
以下のコードは、条件変数を使用してスレッドを同期する例を示しています。一部のスレッドの操作中(「ワーカー」と呼びましょう)、キューに配置されているときにエラーが発生する場合があります。 「レジストラ」スレッドは、これらのエラーを処理(キューから取得)し、印刷します。 エラーが発生すると、「Workers」は「registrar」に信号を送ります。 レジストラは、条件付き変数信号を予期しています。 誤った覚醒を回避するために、ブール条件がチェックされるループで予期が発生します。
#include <condition_variable> #include <iostream> #include <random> #include <thread> #include <mutex> #include <queue> std::mutex g_lockprint; std::mutex g_lockqueue; std::condition_variable g_queuecheck; std::queue<int> g_codes; bool g_done; bool g_notified; void workerFunc(int id, std::mt19937 &generator) { // { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[worker " << id << "]\trunning..." << std::endl; } // std::this_thread::sleep_for(std::chrono::seconds(1 + generator() % 5)); // int errorcode = id*100+1; { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[worker " << id << "]\tan error occurred: " << errorcode << std::endl; } // { std::unique_lock<std::mutex> locker(g_lockqueue); g_codes.push(errorcode); g_notified = true; g_queuecheck.notify_one(); } } void loggerFunc() { // { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[logger]\trunning..." << std::endl; } // , while(!g_done) { std::unique_lock<std::mutex> locker(g_lockqueue); while(!g_notified) // g_queuecheck.wait(locker); // , while(!g_codes.empty()) { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[logger]\tprocessing error: " << g_codes.front() << std::endl; g_codes.pop(); } g_notified = false; } } int main() { // - std::mt19937 generator((unsigned int)std::chrono::system_clock::now().time_since_epoch().count()); // std::thread loggerThread(loggerFunc); // std::vector<std::thread> threads; for(int i = 0; i < 5; ++i) threads.push_back(std::thread(workerFunc, i+1, std::ref(generator))); for(auto &t: threads) t.join(); // g_done = true; loggerthread.join(); return 0; }
このコードを実行すると、おおよそ次の結果が得られます(ワーカースレッドがランダムな時間間隔で動作する(またはスリープする)ため、結果は毎回異なります)。
[logger] running... [worker 1] running... [worker 2] running... [worker 3] running... [worker 4] running... [worker 5] running... [worker 1] an error occurred: 101 [worker 2] an error occurred: 201 [logger] processing error: 101 [logger] processing error: 201 [worker 5] an error occurred: 501 [logger] processing error: 501 [worker 3] an error occurred: 301 [worker 4] an error occurred: 401 [logger] processing error: 301 [logger] processing error: 401
上記の
wait
メソッドには、2つのオーバーロードがあります。
-
unique_lock
のみを使用するunique_lock
。 彼(メソッド)はスレッドをブロックし、この条件変数からのシグナルを待機しているスレッドのキューに追加します。 フローは、条件変数からの信号が受信されたとき、または誤ったウェイクアップの場合にウェイクアップします。 -
unique_lock
に加えて、false
返すまでループで使用される述語を受け入れfalse
。 この過負荷は、誤った覚醒を回避するために使用できます。 一般に、これは次のようなループと同等です。
while(!predicate()) wait(lock);
したがって、2番目のオーバーロードを使用すると、上記の例でブールフラグ
g_notified
使用を回避できます。
void workerFunc(int id, std::mt19937 &generator) { // { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[worker " << id << "]\trunning..." << std::endl; } // std::this_thread::sleep_for(std::chrono::seconds(1 + generator() % 5)); // int errorcode = id*100+1; { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[worker " << id << "]\tan error occurred: " << errorcode << std::endl; } // { std::unique_lock<std::mutex> locker(g_lockqueue); g_codes.push(errorcode); g_queuecheck.notify_one(); } } void loggerFunc() { // { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[logger]\trunning..." << std::endl; } // , while(!g_done) { std::unique_lock<std::mutex> locker(g_lockqueue); g_queuecheck.wait(locker, [&](){return !g_codes.empty();}); // , while(!g_codes.empty()) { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[logger]\tprocessing error: " << g_codes.front() << std::endl; g_codes.pop(); } } }
オーバーロードされた
wait()
メソッドに加えて、述語のオーバーロードが同じ2つの類似したメソッドがあります。
- wait_for :条件変数信号が受信されるまでスレッドをブロックします
- wait_until :条件変数信号が受信されるか、特定の時点に達するまでスレッドをブロックします
述語なしでこれらのメソッドをオーバーロードすると、タイムアウトが発生したのか、条件変数信号によるウェイクアップが発生したのか、それが偽のウェイクアップであるのかを示すcv_statusが返されます。
Stdは、 notify_all_at_thread_exit関数も提供します。 この関数は、すべての
thread_local
オブジェクトの破棄など、データの処理が完了したことを他のスレッドに通知するメカニズムを実装します。
join
以外のメカニズムでスレッドを待機すると、
thread_locals
がすでに使用されており、スレッドがウェイクアップした後、またはスレッドが既に完了した後にデストラクターが呼び出される場合、異常な動作が発生
join
可能性があります( N3070およびN2880を参照してください。通常、呼び出しこの関数は、スレッドが存在し始める直前に実行する必要があります。次に、
notify_all_at_thread_exit
を条件変数とともに使用して2つのスレッドを同期する方法の例を示します。
std::mutex g_lockprint; std::mutex g_lock; std::condition_variable g_signal; bool g_done; void workerFunc(std::mt19937 &generator) { { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "worker running..." << std::endl; } std::this_thread::sleep_for(std::chrono::seconds(1 + generator() % 5)); { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "worker finished..." << std::endl; } std::unique_lock<std::mutex> lock(g_lock); g_done = true; std::notify_all_at_thread_exit(g_signal, std::move(lock)); } int main() { std::mt19937 generator((unsigned int)std::chrono::system_clock::now().time_since_epoch().count()); std::cout << "main running..." << std::endl; std::thread worker(workerFunc, std::ref(generator)); worker.detach(); std::cout << "main crunching..." << std::endl; std::this_thread::sleep_for(std::chrono::seconds(1 + generator() % 5)); { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "main waiting for worker..." << std::endl; } std::unique_lock<std::mutex> lock(g_lock); while(!g_done) // g_signal.wait(lock); std::cout << "main finished..." << std::endl; return 0; }
ワーカーがメインスレッドの前に作業を終了すると、結果は次のようになります。
main running... worker running... main crunching... worker finished... main waiting for worker... main finished...
メインスレッドがワーカースレッドの前に作業を終了すると、結果は次のようになります。
main running... worker running... main crunching... main waiting for worker... worker finished... main finished...
結論として
C ++ 11標準により、C ++開発者は、プラットフォームに依存しない標準的な方法でマルチスレッドコードを記述できます。 この記事は、stdからのストリームと同期メカニズムの概要です。
<thread>
ヘッダーは、スレッドを表す同じ名前(および多くの追加機能)を持つクラスを提供します。
<mutex>
ヘッダーは、スレッドへのアクセスを同期するためのいくつかのミューテックスと「ラッパー」の実装を提供します。
<condition_variable>
ヘッダーは、別のスレッドから通知を受け取るまで、または偽の目覚めの前に、1つ以上のスレッドをブロックできる条件変数の2つの実装を提供します。 より詳細な情報と問題の理解のために、もちろん、追加の文献を読むことをお勧めします:)