文章目录
1. C++11之多线程系列文章目录2. 条件变量(Condition Variable)2.1. 条件变量为什么叫条件变量?2.2. 为什么条件变量需要和锁一起使用?
C++11之多线程系列文章目录一、标准库的线程封装类Thread和Future二、互斥对象(Mutex)和锁(Lock)三、条件变量(Condition Variable)四、原子操作(Atomic Operation)五、内存序(Memory Order)
条件变量(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函数,发现它的两个重载都必须将锁作为参数
123void wait(std::unique_lock
首先考虑wait函数不需要锁作为参数的情况,下面的代码中flag初始化为false,线程A将flag置为true并使用notify_one发出通知,线程B使用while循环在wait前后都会检查flag,直到flag被置为true才往下执行。
123456789101112131415// Thread A{ std::unique_lock lck(mt); flag = true;}cv.notify_one();// Thread Bauto 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函数需要锁作为参数正是为了解决这一问题的。
12345678// Thread Bauto pred = []() { return flag;};std::unique_lock lck(mt);while(!pred()) { cv.wait(lck);}
当线程B调用wait的时候会释放传入的锁并同时进入等待,当被唤醒时会重新获得锁,因此只要保证线程A在修改flag的时候是正确加锁的那么就不会发生前面的这种情况。使用wait函数的另一个重载时下面的代码与上面的6~8行是等价的。
1cv.wait(lck, pred);
不仅仅是C++,就博主所知道的语言但凡有条件变量的概念都必须与锁配合使用。以C#、Java为例
C#
123456789101112131415// Thread Alock (obj){ flag = true; System.Threading.Monitor.Pulse(obj);}// Thread Block (obj){ while(!pred()) { System.Threading.Monitor.Wait(obj); }}
Java
123456789101112// Thread Asynchronized(obj) { flag = true; obj.notify();}// Thread Bsynchronized(obj) { while(!pred()) { obj.wait(); }}
C#与C++不同之处在于C#在Pulse或PulseAll的线程必须持有锁,而C++的notify_one和notify_all则无所谓是否持有锁。