条件变量(Condition Variable)
条件变量
是一种同步原语(Synchronization Primitive)用于多线程之间的通信,它可以阻塞一个或同时阻塞多个线程直到:
- 收到来自其他线程的通知
- 超时
- 发生虚假唤醒(Spurious Wakeup)
C++11为条件变量提供了两个类
- std::condition_variable:必须与std::unique_lock配合使用
- std::condition_variable_any:更加通用的条件变量,可以与任意类型的锁配合使用,相比前者使用时会有额外的开销
二者具有相同的成员函数
成员函数 | 说明 |
---|---|
notify_one | 通知一个等待线程 |
notify_all | 通知全部等待线程 |
wait | 阻塞当前线程直到被唤醒 |
wait_for | 阻塞当前线程直到被唤醒或超过指定的等待时间(长度) |
wait_until | 阻塞当前线程直到被唤醒或到达指定的时间(点) |
二者在线程要等待条件变量前都必须要获得相应的锁
条件变量为什么叫条件变量?
- 条件变量存在虚假唤醒的情况,因此在线程被唤醒后需要检查条件是否满足
- 无论是notify_one或notify_all都是类似于发出脉冲信号,如果对wait的调用发生在notify之后是不会被唤醒的,所以接收者在使用wait等待之前也需要检查条件(标识)是否满足,另一个线程(通知者)在nofity前需要修改相应标识供接收者检查
条件变量因此得名。
为什么条件变量需要和锁一起使用?
观察std::condition_variable::wait函数,发现它的两个重载都必须将锁作为参数
1 2 3 |
void wait(std::unique_lock<std::mutex>& lock); template< class Predicate > void wait(std::unique_lock<std::mutex>& lock, Predicate pred); |
首先考虑wait函数不需要锁作为参数的情况,下面的代码中flag初始化为false,线程A将flag置为true并使用notify_one发出通知,线程B使用while循环在wait前后都会检查flag,直到flag被置为true才往下执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Thread A { std::unique_lock lck(mt); flag = true; } cv.notify_one(); // Thread B auto pred = []() { std::unique_lock lck(mt); return flag; }; while(!pred()) { cv.wait(); } |
如果两个线程的执行顺序为:
- 线程B检查flag发现其值为false
- 线程A将flag置为true
- 线程A使用notify_one发出通知
- 线程B使用wait进行等待
那么线程B将不会被唤醒(即线程B没有察觉到线程A发出的通知),这显然不是程序员想要的结果,发生这种情况的根源在于线程B对条件的检查和进入等待的中间是有空档的。wait函数需要锁作为参数正是为了解决这一问题的。
1 2 3 4 5 6 7 8 |
// Thread B auto pred = []() { return flag; }; std::unique_lock lck(mt); while(!pred()) { cv.wait(lck); } |
当线程B调用wait的时候会释放传入的锁并同时进入等待,当被唤醒时会重新获得锁,因此只要保证线程A在修改flag的时候是正确加锁的那么就不会发生前面的这种情况。
使用wait函数的另一个重载时下面的代码与上面的6~8行是等价的。
1
|
cv.wait(lck, pred);
|
不仅仅是C++,就博主所知道的语言但凡有条件变量的概念都必须与锁配合使用。以C#、Java为例
扫描二维码关注公众号,回复:
1661422 查看本文章
C#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Thread A lock (obj) { flag = true; System.Threading.Monitor.Pulse(obj); } // Thread B lock (obj) { while(!pred()) { System.Threading.Monitor.Wait(obj); } } |
Java
1 2 3 4 5 6 7 8 9 10 11 12 |
// Thread A synchronized(obj) { flag = true; obj.notify(); } // Thread B synchronized(obj) { while(!pred()) { obj.wait(); } } |
C#与C++不同之处在于C#在Pulse或PulseAll的线程必须持有锁,而C++的notify_one和notify_all则无所谓是否持有锁。