CPP——并发编程(四)condition_variable

到目前为止介绍的并发过程都是类似于去“争夺”控制,也就是只要有机会就去上,但是有时候我们会遇到那种等待的情况,达到某个条件了才能运行,这时候就需要用条件变量(condition_variable)。

关于条件变量的内容都在头文件中。它包含了std::condition_variable,std::condition_variable_any,还有枚举类型std::cv_status,函数std::notify_all_at_thread_exit()。这篇文章会介绍上面这些类以及函数的作用。

std::condition_variable

condition_variable的设定是这样的,它调用wait函数,使用unique_lock锁住当前线程,表明条件还未满足,这时候当前线程会被阻塞,直到另外一个线程在相同的condition_variable变量上调用了notification相关的函数,说明条件满足,则开始运行。

condition_variable只有默认构造函数,拷贝构造函数被删除。下面介绍它其他的成员函数。

wait

condition_variable有两个wait函数的重载。

1
2
3
void wait (unique_lock<mutex>& lck);// unconditional
template <class Predicate> //predicate
void wait (unique_lock<mutex>& lck, Predicate pred);

wait接受一个参数,类型是unique_lock,调用wait函数后,当前线程被阻塞(当前线程已经获得了mutex的控制权,也就是mutex已经被锁住了),直到该condition_variable变量被notify之后,才能继续运行wait之后的内容。当线程被阻塞时,wait会调用unique_lock的unlock对mutex进行解锁,使得其他的线程可以占用mutex。当被notify之后,wait会调用lock来继续锁住mutex(如果此时被占用,当前线程继续被阻塞)。

对于predicate的wait,只有当predicate条件为false时候调用wait才会阻塞当前线程,并且只有当其他的线程通知predicate为true时才会解除阻塞,相当于又多了一个条件。predicate是一个callable的对象,不接受参数,并且能返回值可以转化成bool值。相当于:

1
while (!pred()) wait(lck);

wait_for

wait_for与wait一样,也有两个重载。

1
2
3
4
5
6
template <class Rep, class Period>//unconditional
cv_status wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time);
template <class Rep, class Period, class Predicate>//predicate
bool wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time, Predicate pred);

wait_for和wait的区别在于它会接受一个时间段,在当前线程收到通知,或者在指定的时间内,它都会阻塞,当超时,或者收到了通知达到条件,wait_for返回,剩下的和wait一致。

wait_until

wait_until和wait_for类似,只不过时间范围被替换成了时间点。

1
2
3
4
5
6
7
template <class Clock, class Duration>//unconditional
cv_status wait_until (unique_lock<mutex>& lck,
const chrono::time_point<Clock,Duration>& abs_time);
template <class Clock, class Duration, class Predicate>
bool wait_until (unique_lock<mutex>& lck,//predicate
const chrono::time_point<Clock,Duration>& abs_time,
Predicate pred);

notify_one

唤醒某个等待线程,如果没有线程在等待,那么这个函数什么也不做,如果有多个线程等待,唤醒哪个线程是随机的。

notify_all

唤醒所有的等待线程。

std::condition_variable_any

condition_variable_any和condition_variable的区别是wait接受的锁是任意的,而condition_variable只能接受unique_lock。

std::cv_status

这个枚举类型仅仅枚举了两个状态,标志着wait_for或者wait_until是否是因为超时解除阻塞的。

描述
cv_status::no_timeout wait_for 或者 wait_until 没有超时,即在规定的时间段内线程收到了通知
cv_status::timeout wait_for 或者 wait_until 超时

std::notify_all_at_thread_exit

notify_all_at_thread_exit是一个函数,原型如下:

1
void notify_all_at_thread_exit (condition_variable& cond, unique_lock<mutex> lck);

当调用该函数的线程结束时,所有在cond上等待的线程都会收到通知。

总体来说,条件变量的相关内容还是很容易理解的。现在研究一下下面的代码:

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 <iostream>                // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable

std::mutex mtx; // global mutex
std::condition_variable cv; // global condition variable.
bool ready = false; // global flag.

void do_print_id(int id)
{
std::unique_lock <std::mutex> lck(mtx);
if (!ready)
cv.wait(lck);
std::cout << "thread " << id << '\n';
}

void go()
{
std::unique_lock <std::mutex> lck(mtx);
ready = true;
cv.notify_all();
}

int main()
{
std::thread threads[10];
// spawn 10 threads:
for (int i = 0; i < 10; ++i)
threads[i] = std::thread(do_print_id, i);

std::cout << "10 threads ready to race...\n";
go(); // go!

for (auto & th:threads)
th.join();

return 0;
}

10个线程被发出去后,都会因为ready是false而被wait阻塞,当ready为true时,唤醒了所有的线程,这时候线程之间的控制就又转成了对互斥量mutex的控制,因此输出顺序是不一致的。可能输出如下:

1
2
3
4
5
6
7
8
9
10
11
10 threads ready to race...
thread 7
thread 3
thread 1
thread 9
thread 4
thread 5
thread 6
thread 8
thread 2
thread 0