cpp wait and condition variable

It always takes me lots of time to figure out details regarding to the wait, notify, and notify_all etc. This article contains several examples and we dive into details to figure out how they work.

notify_all example

example of wait and notify_all (https://en.cppreference.com/w/cpp/thread/condition_variable/wait)

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
#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>

std::condition_variable cv;
std::mutex cv_m; // This mutex is used for three purposes:
// 1) to synchronize accesses to i
// 2) to synchronize accesses to std::cerr
// 3) for the condition variable cv
int i = 0;

void waits()
{
std::unique_lock<std::mutex> lk(cv_m);
std::cerr << "Waiting... \n";
cv.wait(lk, []{return i == 1;});
std::cerr << "...finished waiting. i == 1\n";
}

void signals()
{
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lk(cv_m);
std::cerr << "Notifying...\n";
}
// this is the false notifying since the indicator is not updated
cv.notify_all();

std::this_thread::sleep_for(std::chrono::seconds(1));

{
std::lock_guard<std::mutex> lk(cv_m);
i = 1;
std::cerr << "Notifying again...\n";
}
// this notify is ok, since the indicator has been modified
cv.notify_all();
}

int main()
{
std::thread t1(waits), t2(waits), t3(waits), t4(signals);
t1.join();
t2.join();
t3.join();
t4.join();
}

It is better to remember this basic model and when there are differnet actual problem, and we can always map the actual problem into the model.

There are three parameter that is important:(1) the varaible that is shared by different thread, such as if things is ready, we also call this varaible as indicator (2) the mutex that control the atomicity of this indicator. (3) the condition variable that is used to control the execution sequence.

One thing that we should pay extra attention is the wait operation, one of its parameter is the lock, that indicates that the lock should be required before executing the wait function, then in the wait operation, at the moment of blocking the thread, the function automatically calls lck.unlock(), it release the lock and allows other locked threads to continue.

Just pay attention to the concepts of the block and lock, when we say block, it means the thread seemingly hangs there. In particular, the blocking call is to suspend the process without obtaining the resource, and the suspended process enters the sleep state, refer to this. When we say the lock, it is just the programming machnism that avoid the race condition. Only the critical section that acquire the lock can execute associated section. The lock may cause the sleep, especially when the current lock is acquired by another thread, but it depends on the implementation of the lock, refer to this, for the spin lock, the other thread just check it periodically without changing the status, but this thread also looks like blocked.

So let’s come back to the wait operation. Once the wait is notified by associated condition varaibe, it unblocks (the thread become active again) and calls lck.lock(), leaving lck in the same state as when the function was called. Then the function returns (notice that this last mutex locking may block again the thread before returning). Basically, the wait operation release the lock and put the lock back transparently to user.

With this in mind, the code list above is easy to understand, first step is to figure out several basic elements: the indicator value is the variable i here, there is also an mutex cv_m and a condition varaible cv.

Then in the thread which is designed for waiting sth, there is a std::unique_lock<std::mutex> lk(cv_m) which is the simplified lock and unlock operation in c++, this can guarantee that lock is released autoamtically out of the scope. Let’s check the execution results of this program:

1
2
3
4
5
6
7
8
Waiting... 
Waiting...
Waiting...
Notifying...
Notifying again...
...finished waiting. i == 1
...finished waiting. i == 1
...finished waiting. i == 1

The first three thread starts and move to the wait operation, these threads suspend and wait for the signal from other thread. Then the signal thread moves to the notify_all, attention, when signal thread execute the notify_all, the lock should be released. otherwise, the wait thread can not check the indicator varaible. When the notify_all is called, the wait opertaion associated with the cv varaible start its job, it checks the status of indicator and then lock it if it is not satisfied. Then it comes back to the orginal status since the indicator varaible is not modified. So it suspends itsself and lock things again.

Then when the signal thread comes to the notify position the second time, since the indicator is actually updated this time, the wait operation pass, it acquires lock again and all waits thread finish processing.

the lost wakeup code

For this 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
void waitingForWork1() {
sleep(1);
std::cout << "Waiting " << std::endl;
std::unique_lock<std::mutex> lck(mutex_);
condVar.wait(lck);
std::cout << "Running " << std::endl;
}

void setDataReady() {
std::cout << "Data prepared" << std::endl;
{
std::unique_lock<std::mutex> lck(mutex_);
dataReady = true;
}
condVar.notify_one();
}

int main() {
std::cout << std::endl;

std::thread t1(waitingForWork1);
std::thread t2(setDataReady);

t1.join();
t2.join();

std::cout << std::endl;
}

/*output:
Data prepared
Waiting
*/

The waiting thread waits here forever, since the notify comes earlyer then the wait, and we also did not use the indicator variable here. The wait thread can only unblock the current thread when it is called earlier than the notify operations.

We can correct it by adding the indicator and related condition checking operations:

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
#include <unistd.h>
#include <condition_variable>
#include <iostream>
#include <thread>

std::mutex mutex_;
std::condition_variable condVar;
bool dataReady = false;

void waitingForWork2() {
sleep(1);
std::cout << "Waiting " << std::endl;
std::unique_lock<std::mutex> lck(mutex_);
while (dataReady == false) {
condVar.wait(lck);
}
std::cout << "Running " << std::endl;
}

void setDataReady() {
std::cout << "Data prepared" << std::endl;
{
std::unique_lock<std::mutex> lck(mutex_);
dataReady = true;
}
condVar.notify_one();
}

int main() {
std::cout << std::endl;

std::thread t1(waitingForWork2);
std::thread t2(setDataReady);

t1.join();
t2.join();

std::cout << std::endl;
}

When the program comes to the while part, it blocks there since the indicator is false, wait call will block the thread, then until the notify is called, the wait moves forward. Of course, we can also use the wait call with a predicate parameter. The reason that we use the while instead of the if to detect the indicator value is to avoid the spirious wakeup, refer to this

references

https://www.jianshu.com/p/0eff666a4875

https://stackoverflow.com/questions/8594591/why-does-pthread-cond-wait-have-spurious-wakeups

https://www.modernescpp.com/index.php/c-core-guidelines-be-aware-of-the-traps-of-condition-variables

https://en.cppreference.com/w/cpp/thread/condition_variable/wait

推荐文章