1. 概述 
条件变量(condition variable)是利用共享的变量进行线程之间同步的一种机制。典型的场景包括生产者-消费者模型,线程池实现等。 
对条件变量的使用包括两个动作: 
1) 线程等待某个条件, 条件为真则继续执行,条件为假则将自己挂起(避免busy wait,节省CPU资源); 
2) 线程执行某些处理之后,条件成立;则通知等待该条件的线程继续执行。 
3) 为了防止race-condition,条件变量总是和互斥锁变量mutex结合在一起使用。 
一般的编程模式:

  1. var mutex;
  2. var cond;
  3. var something;
  4. Thread1: (等待线程)
  5. lock(mutex);
  6. while( something not true ){
  7. condition_wait( cond, mutex);
  8. }
  9. do(something);
  10. unlock(mutex);
  11. //============================
  12. Thread2: (解锁线程)
  13. do(something);
  14. ....
  15. something = true;
  16. unlock(mutex);
  17. condition_signal(cond);

函数说明: 
(1) Condition_wait():调用时当前线程立即进入睡眠状态,同时互斥变量mutex解锁(这两步操作是原子的,不可分割),以便其它线程能进入临界区修改变量。 
(2) Condition_signal(): 线程调用此函数后,除了当前线程继续往下执行以外; 操作系统同时做如下动作:从condition_wait()中进入睡眠的线程中选一个线程唤醒, 同时被唤醒的线程试图锁(lock)住互斥量mutex, 当成功锁住后,线程就从condition_wait()中成功返回了。

2. 函数接口

  1. pthread: pthread_cond_wait/pthread_cond_signal/pthread_cond_broadcast()
  2. Java: Condition.await()/Condition.signal()/Condition.signalAll()

3. 虚假唤醒(spurious wakeup)在采用条件等待时,我们使用的是

  1. while(条件不满足){
  2. condition_wait(cond, mutex);
  3. }
  4. 而不是:
  5. If( 条件不满足 ){
  6. Condition_wait(cond,mutex);
  7. }

这是因为可能会存在虚假唤醒”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()中返回了。

  1. //...
  2. unlock(mutex);
  3. condition_signal(cond);

(2) 按照 condition_signal(); unlock(mutext)顺序,当等待线程被唤醒时,它试图锁住mutex,但是如果此时mutex还未解锁,则线程又进入睡眠,mutex成功解锁后,此线程在再次被唤醒并锁住mutex,从而从condition_wait()中返回。

  1. //...
  2. condition_signal(cond);
  3. 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继续等待。

多线程编程中条件变量和的spurious wakeup 虚假唤醒的更多相关文章

  1. Linux多线程编程的条件变量

    在stackoverflow上看到一关于多线程条件变量的问题,题主问道:什么时候会用到条件变量,mutex还不够吗?有个叫slowjelj的人做了很好的回答,我再看这个哥们其他话题的一些回答,感觉水平 ...

  2. python多线程编程5: 条件变量同步-乾颐堂

    互斥锁是最简单的线程同步机制,Python提供的Condition对象提供了对复杂线程同步问题的支持.Condition被称为条件变量,除了提供与Lock类似的acquire和release方法外,还 ...

  3. Linux 多线程编程—使用条件变量实现循环打印

    编写一个程序,开启3个线程,这3个线程的ID分别为A.B.C,每个线程将自己的ID在屏幕上打印10遍,要求输出结果必须按ABC的顺序显示:如:ABCABC….依次递推. 使用条件变量来实现: #inc ...

  4. 转载~kxcfzyk:Linux C语言多线程库Pthread中条件变量的的正确用法逐步详解

    Linux C语言多线程库Pthread中条件变量的的正确用法逐步详解   多线程c语言linuxsemaphore条件变量 (本文的读者定位是了解Pthread常用多线程API和Pthread互斥锁 ...

  5. linux多线程同步pthread_cond_XXX条件变量的理解

    在linux多线程编程中,线程的执行顺序是不可预知的,但是有时候由于某些需求,需要多个线程在启动时按照一定的顺序执行,虽然可以使用一些比较简陋的做法,例如:如果有3个线程 ABC,要求执行顺序是A-- ...

  6. Java多线程编程(2)--多线程编程中的挑战

    一.串行.并发和并行   为了更清楚地解释这三个概念,我们来举一个例子.假设我们有A.B.C三项工作要做,那么我们有以下三种方式来完成这些工作:   第一种方式,先开始做工作A,完成之后再开始做工作B ...

  7. linux C++ 多线程使用pthread_cond 条件变量

    1. 背景 多线程中经常需要使用到锁(pthread_mutex_t)来完成多个线程之间的互斥操作. 但是互斥锁有一个明显到缺点: 只有两种状态,锁定和非锁定. 而条件变量则通过允许线程阻塞并等待另一 ...

  8. C#多线程编程中的锁系统

    C#多线程编程中的锁系统(二) 上章主要讲排他锁的直接使用方式.但实际当中全部都用锁又太浪费了,或者排他锁粒度太大了. 这一次我们说说升级锁和原子操作. 目录 1:volatile 2:  Inter ...

  9. 关于python多线程编程中join()和setDaemon()的一点儿探究

    关于python多线程编程中join()和setDaemon()的用法,这两天我看网上的资料看得头晕脑涨也没看懂,干脆就做一个实验来看看吧. 首先是编写实验的基础代码,创建一个名为MyThread的  ...

随机推荐

  1. git忽略特殊文件或文件夹

    1.在项目目录中添加“.gitignore”文件,项目目录就是你存放git工程的目录就是有“.git”目录的目录 vi .gitignore 2.在文件中添加如下内容,其中“/runtime/”是忽略 ...

  2. java 学习网站

    http://how2j.cn/  教学网站 慕课视频下载网站 http://www.feemic.cn/mooc //慕课搜索和下载的网站http://www.soshoulu.com/tools/ ...

  3. 调研助力4S店,解码困境谜团

    关键词————4S店.汽车.销售.精准营销.闭环.用户满意度.精细化管理 一.背景 4S店是“四位一体”的汽车销售专卖店,包括了整车销售.零配件供应.售后服务.信息反馈四项功能. 信息化管理 精细化管 ...

  4. 【考古向翻译】Pwn2Own 2010 Windows 7 Internet Explorer 8 exploit

    正好在Google搜到了这篇文章,就打算自己翻译一下,也不清楚国内是否有人已经翻译过了.作者是Pwn2Own 2010的获奖者来自荷兰的皮特·维莱格登希尔(Peter Vreugdenhil). 20 ...

  5. ***四种参数传递的形式——URL,超链接,js,form表单

    什么时候用GET,  查,删 什么时候用POST,增,改  (特列:登陆用Post,因为不能让用户名和密码显示在URL上) 4种get传参方式 <html xmlns="http:// ...

  6. **PHP删除数组中特定元素的两种方法array_splice()和unset()

    方法一: 复制代码代码如下: <?php$arr1 = array(1,3, 5,7,8);$key = array_search(3, $arr1); if ($key !== false)  ...

  7. drools7 (二、agenda-group 的使用)

    几个关键点: 1. 如果没有指定agenda-group 则默认把所有未指定agenda-group的 rules 都执行一遍 2. 如果指定了agenda-group 使用的时候必须指定该name才 ...

  8. 异常类Exception(String message, Throwable cause)中的cause理解

    这个在构造函数里面竟然有一个Throwable,感觉有些奇怪. 1. Throwable cause 这里cause要传一个Throwable的子类异常进去么? 是引起这个异常的异常,如果这个值是空值 ...

  9. 004 爬虫(最初的爬虫方式,以及urllib2)

    一:最初的爬取方式 1.代码示例 # coding=utf-8 import urllib2 request=urllib2.Request("http://www.baidu.com&qu ...

  10. 取得项目的 Git 仓库

    有两种取得 Git 项目仓库的方法.第一种是在现存的目录下,通过导入所有文件来创建新的 Git 仓库.第二种是从已有的 Git 仓库克隆出一个新的镜像仓库来. 在工作目录中初始化新仓库 要对现有的某个 ...