In the previous post in this series we have seen the C++11 support for locks and in this post we continue on this topic with condition variables. A condition variable is a synchronization primitive that enables blocking of one or more threads until either a notification is received from another thread or a timeout or a spurious wake-up occurs.

There are two implementations of a condition variable that are provided by C++11:

condition_variable: requires any thread that wants to wait on it to acquire a std::unique_lock first.

first. condition_variable_any: is a more general implementation that works with any type that satisfies the condition of a basic lock (basically has a lock() and unlock() method). This might be more expensive to use (in terms of performance and operating system resources), therefore it should be preferred only if the additional flexibility it provides is necessary.

So how does a condition variable work?

There must be at least one thread that is waiting for a condition to become true. The waiting thread must first acquire a unique_lock . This lock is passed to the wait() method, that releases the mutex and suspends the thread until the condition variable is signaled. When that happens the thread is awaken and the lock is re-acquired.

. This lock is passed to the method, that releases the mutex and suspends the thread until the condition variable is signaled. When that happens the thread is awaken and the lock is re-acquired. There must be at least one thread that is signaling that a condition becomes true. The signaling can be done with notify_one() which unblocks one thread (any) that is waiting for the condition to be signaled or with notify_all which unblocks all the threads that are waiting for the condition.

Because of some complications in making the condition wake-up completely predictable on multiprocessor systems, spurious wake-ups can occur. That means a thread is awaken even if nobody signaled the condition variable. Therefore it is necessary to check if the condition is still true after the thread has awaken. And since spurious wake-ups can occur multiple times, that check must be done in a loop.

The code below shows an example of using a condition variable to synchronize threads: several “worker” threads may produce an error during their work and they put the error code in a queue. A “logger” thread processes these error codes, by getting them from the queue and printing them. The workers signal the logger when an error occurred. The logger is waiting on the condition variable to be signaled. To avoid spurious wakeups the wait happens in a loop where a boolean condition is checked.

#include <thread> #include <mutex> #include <condition_variable> #include <iostream> #include <queue> #include <random> 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) { // print a starting message { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[worker " << id << "]\trunning..." << std::endl; } // simulate work std::this_thread::sleep_for(std::chrono::seconds(1 + generator() % 5)); // simulate error int errorcode = id*100+1; { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[worker " << id << "]\tan error occurred: " << errorcode << std::endl; } // notify error to be logged { std::unique_lock<std::mutex> locker(g_lockqueue); g_codes.push(errorcode); g_notified = true; g_queuecheck.notify_one(); } } void loggerfunc() { // print a starting message { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[logger]\trunning..." << std::endl; } // loop until end is signaled while(!g_done) { std::unique_lock<std::mutex> locker(g_lockqueue); while(!g_notified) // used to avoid spurious wakeups { g_queuecheck.wait(locker); } // if there are error codes in the queue process them 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() { // initialize a random generator std::mt19937 generator((unsigned int)std::chrono::system_clock::now().time_since_epoch().count()); // start the logger std::thread loggerthread(loggerfunc); // start the working threads std::vector<std::thread> threads; for(int i = 0; i < 5; ++i) { threads.push_back(std::thread(workerfunc, i+1, std::ref(generator))); } // work for the workers to finish for(auto& t : threads) t.join(); // notify the logger to finish and wait for it g_done = true; loggerthread.join(); return 0; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 #include <thread> #include <mutex> #include <condition_variable> #include <iostream> #include <queue> #include <random> 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 ) { // print a starting message { std :: unique_lock < std :: mutex > locker ( g_lockprint ) ; std :: cout << "[worker " << id << "]\trunning..." << std :: endl ; } // simulate work std :: this_thread :: sleep_for ( std :: chrono :: seconds ( 1 + generator ( ) % 5 ) ) ; // simulate error int errorcode = id * 100 + 1 ; { std :: unique_lock < std :: mutex > locker ( g_lockprint ) ; std :: cout << "[worker " << id << "]\tan error occurred: " << errorcode << std :: endl ; } // notify error to be logged { std :: unique_lock < std :: mutex > locker ( g_lockqueue ) ; g_codes . push ( errorcode ) ; g_notified = true ; g_queuecheck . notify_one ( ) ; } } void loggerfunc ( ) { // print a starting message { std :: unique_lock < std :: mutex > locker ( g_lockprint ) ; std :: cout << "[logger]\trunning..." << std :: endl ; } // loop until end is signaled while ( ! g_done ) { std :: unique_lock < std :: mutex > locker ( g_lockqueue ) ; while ( ! g_notified ) // used to avoid spurious wakeups { g_queuecheck . wait ( locker ) ; } // if there are error codes in the queue process them 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 ( ) { // initialize a random generator std :: mt19937 generator ( ( unsigned int ) std :: chrono :: system_clock :: now ( ) . time_since_epoch ( ) . count ( ) ) ; // start the logger std :: thread loggerthread ( loggerfunc ) ; // start the working threads std :: vector < std :: thread > threads ; for ( int i = 0 ; i < 5 ; ++ i ) { threads . push_back ( std :: thread ( workerfunc , i + 1 , std :: ref ( generator ) ) ) ; } // work for the workers to finish for ( auto & t : threads ) t . join ( ) ; // notify the logger to finish and wait for it g_done = true ; loggerthread . join ( ) ; return 0 ; }

Running this code produce an output that looks like this (notice this output is different with each run because each worker thread works, i.e. sleeps, for a random interval):

[logger] running... [worker 1] running... [worker 2] running... [worker 3] running... [worker 4] running... [worker 5] running... [worker 1] an error occurred: 101 [worker 3] an error occurred: 301 [worker 2] an error occurred: 201 [logger] processing error: 101 [logger] processing error: 301 [logger] processing error: 201 [worker 5] an error occurred: 501 [logger] processing error: 501 [worker 4] an error occurred: 401 [logger] processing error: 401 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [ logger ] running . . . [ worker 1 ] running . . . [ worker 2 ] running . . . [ worker 3 ] running . . . [ worker 4 ] running . . . [ worker 5 ] running . . . [ worker 1 ] an error occurred : 101 [ worker 3 ] an error occurred : 301 [ worker 2 ] an error occurred : 201 [ logger ] processing error : 101 [ logger ] processing error : 301 [ logger ] processing error : 201 [ worker 5 ] an error occurred : 501 [ logger ] processing error : 501 [ worker 4 ] an error occurred : 401 [ logger ] processing error : 401

The wait() method seen above has two overloads:

one that only takes a unique_lock ; this one releases the lock, blocks the thread and adds it to the queue of threads that are waiting on this condition variable; the thread wakes up when the the condition variable is signaled or when a spurious wakeup occurs. When any of those happen, the lock is reacquired and the function returns.

; this one releases the lock, blocks the thread and adds it to the queue of threads that are waiting on this condition variable; the thread wakes up when the the condition variable is signaled or when a spurious wakeup occurs. When any of those happen, the lock is reacquired and the function returns. one that in addition to the unique_lock also takes a predicate that is used to loop until it returns false; this overload may be used to avoid spurious wakeups. It is basically equivalent to:

while(!predicate()) wait(lock); 1 2 while ( ! predicate ( ) ) wait ( lock ) ;

As a result the use of the boolean flag g_notified in the example above can be avoided by using the wait overload that takes a predicate that verifies the state of the queue (empty or not):

void workerfunc(int id, std::mt19937& generator) { // print a starting message { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[worker " << id << "]\trunning..." << std::endl; } // simulate work std::this_thread::sleep_for(std::chrono::seconds(1 + generator() % 5)); // simulate error int errorcode = id*100+1; { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[worker " << id << "]\tan error occurred: " << errorcode << std::endl; } // notify error to be logged { std::unique_lock<std::mutex> locker(g_lockqueue); g_codes.push(errorcode); g_queuecheck.notify_one(); } } void loggerfunc() { // print a starting message { std::unique_lock<std::mutex> locker(g_lockprint); std::cout << "[logger]\trunning..." << std::endl; } // loop until end is signaled while(!g_done) { std::unique_lock<std::mutex> locker(g_lockqueue); g_queuecheck.wait(locker, [&](){return !g_codes.empty();}); // if there are error codes in the queue process them 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(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 void workerfunc ( int id , std :: mt19937 & generator ) { // print a starting message { std :: unique_lock < std :: mutex > locker ( g_lockprint ) ; std :: cout << "[worker " << id << "]\trunning..." << std :: endl ; } // simulate work std :: this_thread :: sleep_for ( std :: chrono :: seconds ( 1 + generator ( ) % 5 ) ) ; // simulate error int errorcode = id * 100 + 1 ; { std :: unique_lock < std :: mutex > locker ( g_lockprint ) ; std :: cout << "[worker " << id << "]\tan error occurred: " << errorcode << std :: endl ; } // notify error to be logged { std :: unique_lock < std :: mutex > locker ( g_lockqueue ) ; g_codes . push ( errorcode ) ; g_queuecheck . notify_one ( ) ; } } void loggerfunc ( ) { // print a starting message { std :: unique_lock < std :: mutex > locker ( g_lockprint ) ; std :: cout << "[logger]\trunning..." << std :: endl ; } // loop until end is signaled while ( ! g_done ) { std :: unique_lock < std :: mutex > locker ( g_lockqueue ) ; g_queuecheck . wait ( locker , [ & ] ( ) { return ! g_codes . empty ( ) ; } ) ; // if there are error codes in the queue process them 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 ( ) ; } } }

In addition to this wait() overloaded method there are two more waiting methods, both with similar overloads that take a predicate to avoid spurious wake-ups:

wait_for: blocks the thread until the condition variable is signaled or the specified timeout occurred.

wait_until: blocks the thread until the condition variable is signaled or the specified moment in time was reached.

The overload without a predicate of these two functions returns a cv_status that indicates whether a timeout occurred or the wake-up happened because the condition variable was signaled or because of a spurious wake-up.

The standard also provides a function called notified_all_at_thread_exit that implements a mechanism to notify other threads that a given thread has finished, including destroying all thread_local objects. This was introduced because waiting on threads with other mechanisms than join() could lead to incorrect and fatal behavior when thread_local s were used, since their destructors could have been called even after the waiting thread resumed and possible also finished (see N3070 and N2880 for more). Typically, a call to this function must happen just before the thread exists.

Below is an example of how notify_all_at_thread_exit can be used together with a condition_variable to synchronize two threads:

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() { // initialize a random generator 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) // avoid spurious wake-ups g_signal.wait(lock); std::cout << "main finished..." << std::endl; return 0; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 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 ( ) { // initialize a random generator 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 ) // avoid spurious wake-ups g_signal . wait ( lock ) ; std :: cout << "main finished..." << std :: endl ; return 0 ; }

That would output either (if the worker finishes work before main)

main running... worker running... main crunching... worker finished... main waiting for worker... main finished... 1 2 3 4 5 6 main running . . . worker running . . . main crunching . . . worker finished . . . main waiting for worker . . . main finished . . .

or (if the main finishes work before the worker):

main running... worker running... main crunching... main waiting for worker... worker finished... main finished... 1 2 3 4 5 6 main running . . . worker running . . . main crunching . . . main waiting for worker . . . worker finished . . . main finished . . .

Share this: Twitter

LinkedIn

StumbleUpon

Facebook

Reddit

More

Google

Email



Print

