Condition Variables
Condition variables allow us to synchronize threads via notifications. So, you can implement workflows like sender/receiver or producer/consumer. In such a workflow, the receiver waits for the sender’s notification. If the receiver gets the notification, it continues its work.
std::condition_variable
The condition variable can fulfill the roles of a sender or a receiver. As a sender, it can notify one or more receivers.
That’s all you have to know to use condition variables.
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 |
// conditionVariable.cpp #include <iostream> #include <condition_variable> #include <mutex> #include <thread> std::mutex mutex_; std::condition_variable condVar; void doTheWork(){ std::cout << "Processing shared data." << std::endl; } void waitingForWork(){ std::cout << "Worker: Waiting for work." << std::endl; std::unique_lock<std::mutex> lck(mutex_); condVar.wait(lck); doTheWork(); std::cout << "Work done." << std::endl; } void setDataReady(){ std::cout << "Sender: Data is ready." << std::endl; condVar.notify_one(); } int main(){ std::cout << std::endl; std::thread t1(waitingForWork); std::thread t2(setDataReady); t1.join(); t2.join(); std::cout << std::endl; } |
The program has two child threads: t1 and t2. They get their callable payload (functions or functors) waitingForWork and setDataReady in lines 33 and 34. The function setDataReady notifies – using the condition variable condVar – that it is done with the preparation of the work: condVar.notify_one(). While holding the lock, thread t2 awaits its notification: condVar.wait(lck). The waiting thread always performs the same steps. It wakes up, tries to get the lock, checks if it’s holding it, if the notifications arrived, and, in case of failure, puts itself back to sleep. In case of success, the thread leaves the endless loop and continues with its work.
The output of the program is not so thrilling. That was my first impression. But wait.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
Spurious wakeup
The devil is in the details. It can happen that the receiver finished its task before the sender sent its notification. How is that possible? The receiver is susceptible to spurious wakeups. So the receiver wakes up, although no notification happens. I had to add a predicate to the wait method to protect it from this. That’s precisely what I had done in the following example
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 |
// conditionVariableFixed.cpp #include <iostream> #include <condition_variable> #include <mutex> #include <thread> std::mutex mutex_; std::condition_variable condVar; bool dataReady; void doTheWork(){ std::cout << "Processing shared data." << std::endl; } void waitingForWork(){ std::cout << "Worker: Waiting for work." << std::endl; std::unique_lock<std::mutex> lck(mutex_); condVar.wait(lck,[]{return dataReady;}); doTheWork(); std::cout << "Work done." << std::endl; } void setDataReady(){ std::lock_guard<std::mutex> lck(mutex_); dataReady=true; std::cout << "Sender: Data is ready." << std::endl; condVar.notify_one(); } int main(){ std::cout << std::endl; std::thread t1(waitingForWork); std::thread t2(setDataReady); t1.join(); t2.join(); std::cout << std::endl; } |
The key difference from the first example conditionVariable.cpp is the boolean dataReady used in line 11 as an additional condition. dataReady is set to true in line 28. It is checked in the function waitingForWork: condVar.waint(lck,[]return dataReady;}). The wait() method has an additional overload that accepts a predicate. A predicate is a callable returning true or false. In this example, the callable is a lambda function. So, the condition variable checks whether the predicate is true or if the notification happened.
A short remark – dataReady. dataReady is a shared variable that will be changed. So I had to protect it with a lock. Because thread t1 sets and releases the lock just once, std::lock_guard is fine for that job. That will not hold for thread t2. The wait method will continuously lock and unlock the mutex. So I need the more powerful lock: std::unique_lock.
But that’s not all. Condition variables have a lot of challenges. They must be protected by locks and are susceptible to spurious wakeups. Most use cases are easier to solve with tasks. More about tasks in the next post.
Lost wakeup
The meanness of condition variables goes on. About every 10th execution of the conditionVariable.cpp something strange happens. The program blocks.
I have no idea what’s going on. This phenomenon contradicts my intuition of condition variables. Did I mention that I don’t like condition variables? With the support of Anthony Williams, I solved the riddle.
The problem is that the notification gets lost if the sender sends its notification before the receiver gets to a wait state. The C++ standard describes condition variables as synchronization mechanisms at the same time: “The condition_variable class is a synchronization primitive that can be used to block a thread, or multiple threads at the same time, …“. So the notification gets lost, and the receiver is waiting and waiting and ….
How can this issue be solved? The predicate that got rid of spurious wakeups will also help with lost ones. If the predicate is true, the receiver can continue its work independently of the sender’s notification. The variable dataReady is like a memory. Because as far as the variable data in line 28 is set to true, the receiver assumes in line 21 that the notification was delivered.
What’s next?
With tasks, multithreading in C++ get a lot easier. Stay tuned for the next post. (Proofreader Alexey Elymanov)
Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.
Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.
My special thanks to Embarcadero | |
My special thanks to PVS-Studio | |
My special thanks to Tipi.build | |
My special thanks to Take Up Code | |
My special thanks to SHAVEDYAKS |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!