条件变量
条件变量是一种线程同步手段,一般由三个部分组成:信号,条件和互斥量,他们的作用是这样的:
- 信号的作用是线程间同步,一般会提供两个基本操作,wait 和 notify,wait用于等待信号,notify用于发出信号
- 条件一般是用户自己定义的检查条件。
- 互斥量一是用于保护条件在多线程访问下的一致性,二是用于保护条件变更和信号变更之间的时序,防止漏掉信号或程序死锁。
为什么需要互斥量?
条件变量的机制一般是要求必须在notify之前wait,如果在notify之后wait,会导致notify丢失,wait永远阻塞,这是前提。
使用条件变量时,一般有四种操作(假设条件名为ready):
- 线程1:
- 操作1:wait,等待信号
- 操作2:check ready,检查条件
- 线程2:
- 操作3:ready <- true,变更条件
- 操作4: notify,发出信号
操作3、4的顺序一般是固定的,我们总是会在条件ready变更之后才notify,操作1、2的顺序则不一定,有时候先check ready再 wait,有时候先wait再check ready。
互斥量保护的是条件在多线程下的一致性和操作顺序的原子性,如果操作1、2的顺序永远是先wait再check ready,并且只执行一次,是不需要互斥量的,因为wait被触发时,ready <- true 和 notify 肯定已经执行完毕了,这时候直接check ready肯定是没问题的。
如果先check ready再wait,在没有互斥量的情况下,操作顺序可能会被调度为以下顺序:
- 线程1:check ready
- 线程2:ready <- true
- 线程2:notify
- 线程1:wait
在这种顺序下,check ready在ready <- true之前,因此线程1认为此时不满足条件,需要wait,而wait被调度到了最后,导致接收不到线程2发出的notify信号,陷入永远的阻塞当中。
因此,我们需要使用互斥量将check ready和wait操作绑定到一起,同时为ready <- true操作也上锁,这样,当check ready发生在ready <- true之前时,只有线程1执行完wait,线程2才能获取到锁,执行ready <- true,这样,wait一定发生在notify之前,条件ready也得到了多线程下的访问保护,check ready 和 ready <- true之间不会发生冲突,调度顺序如下:
- 线程1:获取锁
- 线程1:check ready
- 线程1:wait
- 线程1:释放锁并阻塞等待
- 线程2:获取锁
- 线程2:ready <- true
- 线程2:释放锁
- 线程2:notify
那什么情况下会先check ready再wait呢?在循环check ready和wait的时候,例如:
while(!ready)
{
wait();
}
//或
do
{
wait();
}
while(!ready)
条件变量中有三种原子操作
- 锁定 -> 检查条件 -> wait -> 解锁+阻塞等待
- wait唤醒 -> 锁定
- 锁定 -> 更改条件 -> (解锁 -> notify/notify -> 解锁)
以下内容参考https://en.cppreference.com/w/cpp/thread/condition_variable/notify_onenotes部分
注意,在notify之前解锁是为了防止等待线程刚一唤醒就被阻塞,造成性能损失,在一些pthread实现中,会直接在notify的内部实现中把等待线程移到mutex队列中,节省掉notify线程解锁等待线程再加锁这一操作。
有时候也需要notify之后再解锁,防止wait线程把信号给析构了导致在一个被析构的对象上notify
后面说的这种情况根据我的个人理解应该是这么一个调度顺序
- 线程2:获取锁
- 线程2:ready <- true
- 线程2:解锁
- 线程1:获取锁
- 线程1:check ready
- 线程1:满足条件后直接跳过wait,析构信号
- 线程1:解锁
- 线程2:notify后崩溃
这种情况下,需要互斥锁把条件变更和notify操作绑定到一起。
这么看来,文中所提到的一些pthread实现其实更好,既避免了性能损失问题,又避免了信号在notify之前被析构。
C++11 中 的 condition_variable
参考https://zh.cppreference.com/w/cpp/thread/condition_variable
C++11中的条件变量机制叫做condition_variable,头文件是#include <condition_variable>
condition_variable包含三部分,std::mutex、std::condition_variable和一个bool谓词(一个用作条件检查的函数)
condition_variable提供了几种操作:
waitwait_forwait_untilnotify_onenotify_all
其中wait系列操作分别包含两个版本,一个带条件的,一个不带条件的。
在上文提到的条件变量三种原子操作中:
- 锁定 -> 检查条件 -> wait -> 解锁+阻塞等待
- wait唤醒 -> 锁定
- 锁定 -> 更改条件 -> (解锁 -> notify/notify -> 解锁)
wait操作内部整合了检查条件 -> wait -> 解锁+阻塞等待以及wait唤醒 -> 锁定,其他的锁定和解锁动作需要用户自己去完成。
c++条件变量标准写法:
std::mutex muCond;
std::condition_variable cond;
//线程A...
std::unique_lock<std::mutex> ul(muCond);
cond.wait(ul);
//线程B..
std::unique_lock<std::mutex> ul(muCond);
cond.notify_all();
C++的condition_variable不会缓存信号状态,换句话说,如果线程A先notify,线程B后wait,线程B就会错过notify,wait会永远阻塞。
与之相比,windows原生的event就能缓存信号状态,event分两种模式,自动重置和手动重置,自动重置模式的行为基本和C++的condition_variable一致,而手动重置模式下,set event之后,信号状态会保持,即使wait在set event之后被调用,仍能获取到event的set状态,用户如果想要重置event的状态,需要显式再次调用set event去重置event的状态。
如何让C++的condition_variable也能保存状态?这需要用户自定义一个状态,例如
std::mutex muCond;
std::condition_variable cond;
bool flag = false; //用户自定义状态
//线程A...
std::unique_lock<std::mutex> ul(muCond);
//调用wait带谓词版,在谓词中检查条件是否满足并重置状态
//wait会先检查条件是否满足,如果不满足,则阻塞等待
cond.wait(ul,
[&flag]()->bool
{
if (flag) {
flag = false;
return true;
}
return false;
});
//线程B..
std::unique_lock<std::mutex> ul(muCond);
flag = true; //设置状态
cond.notify_all();
带谓词的wait调用本身是为了解决虚假唤醒的问题,当condition_variable被意外唤醒时,wait会先检查谓词是否满足条件,如果不满足,则继续block等待,它的实现等同于
while (!pred())
wait(lock);
它会先检查条件,然后再wait,这样恰好也能解决notify丢失的问题