概述

C++中的条件变量(Conditional Variable)是一种特殊的变量,常用于线程之间的同步。在涉及到诸如:线程因某种原因需要被阻塞,待其满足某些条件时,另一个线程会将该线程唤醒这类应用场景下,条件变量提供了一种优雅的解决方式。

通常,条件变量与锁一同使用,防止数据竞争。

使用流程

如图所示为条件变量的一般使用情景。

条件变量应用情景
条件变量应用情景

在此场景下,待唤醒线程一般处于等待状态,等待满足某些特定条件后被其它线程唤醒;唤醒线程则处理其它功能,并在条件满足的情况下唤醒待唤醒线程。conditional_variable完成了上述通知的功能;shared_data则是不同线程之间的共享缓存区,用于数据共享;mutex则是锁,用于保护conditional_variableshared_data之间的数据同步。

代码接口

条件变量位于标准库<conditional_variable>中,首先需要包含该头文件。头文件中提供了一系列接口,主要分为两个功能:等待通知

函数名称 函数功能
wait 阻塞该线程并等待唤醒
wait_for 阻塞该线程并等待,或者超时
wait_until 阻塞该线程并等待,或者到达某一时间点
notify_one 唤醒一个线程
notify_all 唤醒所有线程

等待函数wait()

函数wait的其中一个函数签名如:

1
wait(unique_lock<mutex>& __lock);

其中,__lock是需要获取的锁。在调用wait()前,当前线程必须已经竞争获得锁,而后进入wait()wait()函数中,会主动释放当前线程获得的锁,并进入阻塞状态;当其它线程获得锁并调用notify_one()notify_any()进行线程唤醒时,当前线程则会退出阻塞状态,并重新试图获取锁。

丢失唤醒和虚假唤醒

上述的wait()函数在实际使用中,可能会遇到丢失唤醒或者虚假唤醒的情况。所谓丢失唤醒,则是在其它线程发出notify时,该线程尚且没有进入等待的阻塞状态,当该线程进入阻塞状态时,已经错过了通知唤醒,进而导致死锁;所谓虚假唤醒,则是由于底层硬件和软件在处理大规模操作时产生的短暂性系统混乱,导致在未收到唤醒通知的情况下提前被唤醒。虚假唤醒的成因是多种多样的,但究其本质都离不开短时间内的内核调度或信号中断等现象,读者可以自行了解。

为了防止上述情况的产生,wait()函数还有一个重载版本,其函数签名如下:

1
2
template<typename _Predicate>
void wait(unique_lock<mutex>& __lock, _Predicate __p);

其中,__p是一个可调用对象,可以是仿函数,也可以是lambda表达式。其相当于一个阻塞条件,当不满足该条件时,当前线程才会被阻塞;反之,则会退出阻塞状态。上述wait()相当于如下的代码段:

1
2
3
while(!__p()){
wait(__lock);
}

因此,如果存在丢失唤醒的情况,则while的判断首先不成立,线程也不会进入阻塞状态;如果存在虚假唤醒的情况,则满足while的循环条件,也就会再次进入阻塞状态。

唤醒函数

conditional_variable中提供了两种唤醒函数,分别为notify_one()notify_all()。两者的唯一区别在于前者是唤醒被当前条件变量阻塞的线程中的一个线程,而后者则是唤醒所有线程。若有多个线程被同一个条件变量阻塞,则notify_one()的唤醒是随机的,也即无法选择具体的唤醒线程。notify内部相当于获取了锁,而后唤醒对应线程,再释放锁。

条件变量的例子

条件变量通常可用于单一或多个生产者,多个消费者的场景。在这种场景下,如果生产者尚未产生足够的数据,则消费者可以进入阻塞状态,降低系统性能消耗;当生产者产生可供消费者消费的数据后,则可以使用条件变量唤醒消费者进行消费。线程池就是一个很好的例子。笔者曾实现过一个线程池,但该线程池没有阻塞功能,导致仅减少了新线程创建时的资源开销。

后续的文章会基于条件变量实现一个基于阻塞队列的线程池。

- -