Condition Variables

Contents[Show]

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 is waiting for the the sender's notification. If the receiver gets the notification, it continues its work.

std::condition_variable

The condition variable can fulfil 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 is waiting for its notification: condVar.wait(lck). The waiting thread always performs same steps. It wakes up, tries to get the lock, checks if he's holding the lock, if the notifications arrived and, in case of failure,  puts himself  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. 

conditionVariable

Spurious wakeup

The devil is in the details. In fact, it can happen, that the receiver finished its task before the sender has sent its notification. How is that possible? The receiver is susceptible for spurious wakeups. So the receiver wakes up, although no notification happens. To protect it from this,  I had to add a predicate to the wait method. That's exactly what I had done in the next 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;}). That's why wait() method has an additional overload which 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 two conditions: if the predicate is true or if the notification happened.

A short remark -  dataReady. dataReady is a shared variable, which 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 for 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.

conditionVariableWithoutPredicate

I have no idea what's going on. This phenomenon contradicts totally 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, if the sender sends its notification before the receiver gets to a wait state, notification gets lost. The C++ standard describes condition variables as synchronization mechanism 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 notfication gets lost and the receiver is waiting and waiting and ... .

How can this issue be solved? The predicate which got rid of spurious wakeups will also help with lost wakeups. In case the predicate is true, the receiver is able to continue its work independently of the notification of the sender. 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 mulitithreading in C++ get a lot easier. Stay tuned for the next post. (Proofreader Alexey Elymanov)

 

 

 

 

 

 

title page smalltitle page small Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library".   Get your e-book. Support my blog.

 

Comments   

0 #71 naias poster contest 2017-01-21 13:29
I'm impressed, I have to admit. Seldom do I come across a blog that's
both equally educative and entertaining, and without a doubt, you have hit
the nail on the head. The issue is something which too few folks are speaking intelligently
about. I am very happy I came across this in my search
for something concerning this.
Quote
0 #72 en.wikipedia.org 2017-01-22 18:18
Wonderful post however I was wondering if you could write
a litte more on this subject? I'd be very grateful if you could elaborate a little bit further.
Kudos!
Quote
0 #73 Penny 2017-03-01 08:47
Hmm iit appears like your site ate my first comment (it
was extrremely long) so I guess I'll juxt sum it up what I submitted annd
say, I'm thoroughly ejoying your blog. I as well am an aspiring blog
writer but I'm still new to the whole thing.
Do you have any recommendations for beginner blog writers?
I'd really appreciate it.
Quote
0 #74 Ashli 2017-03-10 10:46
I would like to thank you for the efforts you've put in writing
this blog. I'm hoping to see the samne high-grade blog psts by youu later on as
well. In fact, your creativbe writing abilities has encouraged me to get my very own website nnow ;)
Quote
0 #75 Carlo 2017-04-09 05:39
That is a good tip especially to those new to the blogosphere.
Brjef but very precise info… Many thanks for sharing this one.
A muust read post!
Quote

Add comment


My Newest E-Book

Latest comments

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 346

All 278869

Currently are 132 guests and no members online