C++ Condition Variable

Introduction

In a multi-threading C++ program, if each thread is working independently, such a program is usually easy to implement and the code is easy to understand.

However, it’s common that the tasks in different threads have dependencies on each other. Thus, some threads will have to wait for other threads to complete the modification of one or more shared variables and notify the threads. In this case, we will have to use std::condition_variable for for scheduling in multi-threading.

In this blog post, I would like to quickly discuss std::condition_variable and some of its caveats.

Example

In this example, we have a master thread and a worker thread that run concurrently. There are some dependencies between the jobs that the two threads are working on.

The master thread will complete some preliminary work and notify the worker thread. The worker thread will then continue the work based on what the master thread has completed. One worker thread has done the work, it will notify the master thread and the master thread will continue to complete all the rest of the work.

wait_notify.cpp
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
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>

std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;

void worker_thread()
{
std::cout << "Worker thread start" << std::endl;
std::unique_lock lk(m);
cv.wait(lk, [] { return ready; });

// After the wait, we own the lock.
std::cout << "Worker thread is processing data" << std::endl;
data += " after processing";

// Send data back to master thread
processed = true;
std::cout << "Worker thread signals data processing completed" << std::endl;

// Manual unlocking is done before notifying, to avoid waking up
// the waiting thread only to block again (see notify_one for details)
lk.unlock();
// The worker thread has done the work,
// Notify the master thread to continue the work.
cv.notify_one();
}

void master_thread()
{
std::cout << "Master thread start" << std::endl;
data = "Example data";
// Send data to the worker thread.
{
std::lock_guard lk(m);
ready = true;
std::cout << "Master thread signals data ready for processing"
<< std::endl;
}
// The mater thread has done the preliminary work,
// Notify the worker thread to continue the work.
cv.notify_one();

// Wait for the worker.
{
std::unique_lock lk(m);
cv.wait(lk, [] { return processed; });
}
std::cout << "Back in master thread, data = " << data << std::endl;
}

int main()
{
std::thread worker(worker_thread), master(master_thread);
// Workflow:
// master thread -> worker thread -> master thread.
worker.join();
master.join();
}

The program will run and the expected output will look like this.

1
2
3
4
5
6
7
8
$ g++ wait_notify.cpp -o wait_notify -std=c++17
$ ./wait_notify
Worker thread start
Master thread start
Master thread signals data ready for processing
Worker thread is processing data
Worker thread signals data processing completed
Back in master thread, data = Example data after processing

FAQ

What If Condition Variable Is Notified Before Before Wait?

According to the notes of notify_one, “This makes it impossible for notify_one() to, for example, be delayed and unblock a thread that started waiting just after the call to notify_one() was made.”

Therefore, to unblock a thread from wait, the condition variable has to be notified after it starts to wait.

For example, the following program will hang forever because the condition variable missed the notification, unless there occurs a spurious wakeup.

notify_before_wait.cpp
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
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>

std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;

void worker_thread()
{
// Sleep the worker thread for 1 second so that the the condition variable
// is notified in the master thread before the conditional variable starts
// to wait in the worker thread.
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Worker thread start" << std::endl;
std::unique_lock lk(m);
// Unless a spurious wakeup occurs, this thread will be blocked forever
// because the conditional variable gets no notification.
cv.wait(lk);

// After the wait, we own the lock.
std::cout << "Worker thread is processing data" << std::endl;
data += " after processing";

// Send data back to master thread
processed = true;
std::cout << "Worker thread signals data processing completed" << std::endl;

// Manual unlocking is done before notifying, to avoid waking up
// the waiting thread only to block again (see notify_one for details)
lk.unlock();
cv.notify_one();
}

void master_thread()
{
std::cout << "Master thread start" << std::endl;
data = "Example data";
// Send data to the worker thread.
{
std::lock_guard lk(m);
ready = true;
std::cout << "Master thread signals data ready for processing"
<< std::endl;
}
// The condition variable is notified before the conditional variable from
// the worker thread and the master thread start to wait.
cv.notify_one();

// Wait for the worker.
{
std::unique_lock lk(m);
// The master thread is blocked the condition variable gets no
// notification.
cv.wait(lk);
}
std::cout << "Back in master thread, data = " << data << std::endl;
}

int main()
{
std::thread worker(worker_thread), master(master_thread);
worker.join();
master.join();
}

The program will hang forever.

1
2
3
4
5
6
$ g++ notify_before_wait.cpp -o notify_before_wait -std=c++17
$ ./notify_before_wait
Master thread start
Master thread signals data ready for processing
Worker thread start

There is a trick to workaround the condition variable notification missing. Instead of calling wait(lock), we could call wait(lock, stop_waiting). The conditional variable will only wait if the predicate stop_waiting returns true.

Therefore, even if the condition variable in the worker thread missed the notification, the thread will not be blocked.

The implementation will be just the same as the first example presented in the article.

notify_before_wait_workaround.cpp
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
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>

std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;

void worker_thread()
{
// Sleep the worker thread for 1 second so that the the condition variable
// is notified in the master thread before the conditional variable starts
// to wait in the worker thread.
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Worker thread start" << std::endl;
std::unique_lock lk(m);
// Even though the wait method is called, the conditional variable wait will
// be skipped if the data is already ready.
cv.wait(lk, [] { return ready; });
// This is equivalent as
// while (!ready())
// {
// wait(lock);
// }

// After the wait, we own the lock.
std::cout << "Worker thread is processing data" << std::endl;
data += " after processing";

// Send data back to master thread
processed = true;
std::cout << "Worker thread signals data processing completed" << std::endl;

// Manual unlocking is done before notifying, to avoid waking up
// the waiting thread only to block again (see notify_one for details)
lk.unlock();
cv.notify_one();
}

void master_thread()
{
std::cout << "Master thread start" << std::endl;
data = "Example data";
// Send data to the worker thread.
{
std::lock_guard lk(m);
ready = true;
std::cout << "Master thread signals data ready for processing"
<< std::endl;
}
// The condition variable is notified before the conditional variable from
// the worker thread and the master thread start to wait.
cv.notify_one();

// Wait for the worker.
{
std::unique_lock lk(m);
cv.wait(lk, [] { return processed; });
}
std::cout << "Back in master thread, data = " << data << std::endl;
}

int main()
{
std::thread worker(worker_thread), master(master_thread);
worker.join();
master.join();
}

The program will run normally.

1
2
3
4
5
6
7
8
$ g++ notify_before_wait_workaround.cpp -o notify_before_wait_workaround -std=c++17
$ ./notify_before_wait_workaround
Master thread start
Master thread signals data ready for processing
Worker thread start
Worker thread is processing data
Worker thread signals data processing completed
Back in master thread, data = Example data after processing

What Is Spurious Wakeup?

Spurious wakeup, by definition, is the blocking thread caused by condition variable wait can be unblocked even without receiving notifications in some situations. This sounds weird and can cause some unexpected behaviors in our program.

To mitigate spurious wakeup, wait(lock, stop_waiting) is very often used because spurious wakeup cannot change the predicate. It is equivalent to

1
2
3
4
while (!stop_waiting())
{
wait(lock);
}

If the predicate is still false, even if there is a spurious wakeup, the condition variable will wait again.

References

Author

Lei Mao

Posted on

08-25-2023

Updated on

08-25-2023

Licensed under


Comments