多线程编程中条件变量和的spurious wakeup 虚假唤醒
1. 概述
条件变量(condition variable)是利用共享的变量进行线程之间同步的一种机制。典型的场景包括生产者-消费者模型,线程池实现等。
对条件变量的使用包括两个动作:
1) 线程等待某个条件, 条件为真则继续执行,条件为假则将自己挂起(避免busy wait,节省CPU资源);
2) 线程执行某些处理之后,条件成立;则通知等待该条件的线程继续执行。
3) 为了防止race-condition,条件变量总是和互斥锁变量mutex结合在一起使用。
一般的编程模式:
- var mutex;
- var cond;
- var something;
- Thread1: (等待线程)
- lock(mutex);
- while( something not true ){
- condition_wait( cond, mutex);
- }
- do(something);
- unlock(mutex);
- //============================
- Thread2: (解锁线程)
- do(something);
- ....
- something = true;
- unlock(mutex);
- condition_signal(cond);
函数说明:
(1) Condition_wait():调用时当前线程立即进入睡眠状态,同时互斥变量mutex解锁(这两步操作是原子的,不可分割),以便其它线程能进入临界区修改变量。
(2) Condition_signal(): 线程调用此函数后,除了当前线程继续往下执行以外; 操作系统同时做如下动作:从condition_wait()中进入睡眠的线程中选一个线程唤醒, 同时被唤醒的线程试图锁(lock)住互斥量mutex, 当成功锁住后,线程就从condition_wait()中成功返回了。
2. 函数接口
- pthread: pthread_cond_wait/pthread_cond_signal/pthread_cond_broadcast()
- Java: Condition.await()/Condition.signal()/Condition.signalAll()
3. 虚假唤醒(spurious wakeup)在采用条件等待时,我们使用的是
- while(条件不满足){
- condition_wait(cond, mutex);
- }
- 而不是:
- If( 条件不满足 ){
- Condition_wait(cond,mutex);
- }
这是因为可能会存在虚假唤醒”spurious wakeup”的情况。
也就是说,即使没有线程调用condition_signal, 原先调用condition_wait的函数也可能会返回。此时线程被唤醒了,但是条件并不满足,这个时候如果不对条件进行检查而往下执行,就可能会导致后续的处理出现错误。
虚假唤醒在linux的多处理器系统中/在程序接收到信号时可能回发生。在Windows系统和JAVA虚拟机上也存在。在系统设计时应该可以避免虚假唤醒,但是这会影响条件变量的执行效率,而既然通过while循环就能避免虚假唤醒造成的错误,因此程序的逻辑就变成了while循环的情况。
注意:即使是虚假唤醒的情况,线程也是在成功锁住mutex后才能从condition_wait()中返回。即使存在多个线程被虚假唤醒,但是也只能是一个线程一个线程的顺序执行,也即:lock(mutex) 检查/处理 condition_wai()或者unlock(mutex)来解锁.
4. 解锁和等待转移(wait morphing)
解锁互斥量mutex和发出唤醒信号condition_signal是两个单独的操作,那么就存在一个顺序的问题。谁先随后可能会产生不同的结果。如下:
[color=red](1) 按照 unlock(mutex); condition_signal()顺序, 当等待的线程被唤醒时,因为mutex已经解锁,因此被唤醒的线程很容易就锁住了mutex然后从conditon_wait()中返回了。
- //...
- unlock(mutex);
- condition_signal(cond);
(2) 按照 condition_signal(); unlock(mutext)顺序,当等待线程被唤醒时,它试图锁住mutex,但是如果此时mutex还未解锁,则线程又进入睡眠,mutex成功解锁后,此线程在再次被唤醒并锁住mutex,从而从condition_wait()中返回。
- //...
- condition_signal(cond);
- unlock(mutex);
[/color]
可以看到,按照(2)的顺序,对等待线程可能会发生2次的上下文切换,严重影响性能。因此在后来的实现中,对(2)的情况,如果线程被唤醒但是不能锁住mutex,则线程被转移(morphing)到互斥量mutex的等待队列中,避免了上下文的切换造成的开销。 -- wait morphing
编程时,推荐采用(1)的顺序解锁和发唤醒信号。而Java编程只能按照(2)的顺序,否则发生异常!!。
在SUSv3http://en.wikipedia.org/wiki/Single_UNIX_Specification的规范中(pthread),指明了这两种顺序不管采用哪种,其实现效果都是一样的。
看过apue大家都知道互斥器用于排他性的访问共享数据而不是等待原语,如果需要等待某个条件发生需要用条件变量。而当用条件变量的时候需要检查某个布尔表达式是否为真,进行这项检查的时候需要互斥器来保护,所以此时互斥器和条件变量联合起来用于同步。
互斥器和条件变量用法如下:
pthread_mutex_lock(&lock);
while (condition_is_false) {
pthread_cond_wait(&cond, &lock);
}
上面那个while能换成if吗?答案是不能,否则会导致spurious wakeup虚假唤醒。因为不仅要在pthread_cond_wait前要检查条件是否成立,在pthread_cond_wait之后也要检查。因为pthread_cond_wait不仅能被pthread_cond_signal/pthread_cond_broadcast唤醒,而且还会被其它信号唤醒,后者就是虚假唤醒。
linux的pthread_cond_wait是用futex系统调用,这个是慢速系统调用,看过apue知道任何慢速系统调用被信号打断的时候会返回-1,并且把errno置为EINTR,如果慢速系统调用的重启功能被关闭,需要在调用该系统调用的地方手动重启它,像下面这样:
while (1) {
int ret = syscall();
if (ret < 0 && errno == EINTR)
continue;
else
break;
}
但是futex不能这么用,因为futex结束后到再次重启这个过程有个时间窗,在这个窗口内可能发生了pthread_cond_signal/phread_cond_broadcast,如果发生这种情况,再进行pthread_cond_wait的时候就错过了一次条件变量的变化,就会无限等待下去。但是如果不像上面那样写又无法重启futex系统调用,咋整呢?这就回到了上面检查布尔条件的时候为什么用while而不用if。
用while不会因为虚假唤醒而错过phread_cond_signal/pthread_cond_broadcast,而且在通过判断while条件不成立检测出此次唤醒为虚假唤醒并继续调用futex继续等待。