一直以来都天真的认为线程间同步的方法只有信号量,互斥量,邮箱,消息队列,知道最近开始研究一些Linux方面的代码才发现自己是多么的很傻很天真。在Linux中还存在这一种叫做条件变量的东西。必须承认我在理解这个概念上花了很多时间,查阅了很多资料。这里主要分析如下几个问题:1. 条件变量是什么;2.为什么要和互斥量配合使用,互斥量保护的是什么;3.为什么条件变量经常会和while配合使用。
1. 什么是条件变量
条件变量是线程同步的一种手段。条件变量用来自动阻塞一个线程,直到条件(predicate)满足被触发为止。通常情况下条件变量和互斥锁同时使用。
条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个/多个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"信号。
2. Mutex的作用
这里让我们先用一段代码说明一般条件变量是如何和互斥量配合使用的:
1 int WaitForPredicate()
2 {
3 // lock mutex (means:lock access to the predicate)
4 pthread_mutex_lock(&mtx);
5
6 // we can safely check this, since no one else should be
7 // changing it unless they have the mutex, which they don't
8 // because we just locked it.
9 while (!predicate)
10 {
11 // predicate not met, so begin waiting for notification
12 // it has been changed *and* release access to change it
13 // to anyone wanting to by unlatching the mutex, doing
14 // both (start waiting and unlatching) atomically
15 pthread_cond_wait(&cv,&mtx);
16 }
17
18 // we own the mutex here. further, we have assessed the
19 // predicate is true (thus how we broke the loop).
20
21 // You *must* release the mutex before we leave.
22 pthread_mutex_unlock(&mtx);
23 }
那这个互斥量保护的是什么呢?是条件变量本身么?并不是!mutex使用来保护predicate。mutex被成功lock后我们就可以放心的去读取predicate的值,而不用担心在这期间predicate会被其他线程修改。如果predicate不满足条件,当前线程阻塞等待其他线程释放条件成立信号,并释放已经lock的mutex。这样一来其他线程就有了修改predicate的机会。当其他线程释放条件成立信号后,pthread_cond_wait函数返回,并再次lock mutex。
pthread_cond_wait的工作流程可以总结为:unlock mutex,start waiting -> lock mutex。
3. while的作用
在上面的代码当中可以看到,predicate是用while来检查的而不是用if在做判断。这样做的原因是,pthread_cond_wait的返回并不一定意味着其他线程释放了条件成立信号。而是意外返回。这种情况称为Spurious wakeup。之所以这样做的原因是从效率上考虑的。Volodya's blog - Spurious wakeups里有很详细的一个讲解,简单来说造成Spurious wakeup的原因在于,Linux中带阻塞功能的system call都会在进程收到了一个signal后返回。这就是为什么要用while来检查的原因。因为我们并不能保证wait函数返回就一定是条件满足,如果条件不满足,还需要继续等待。
4. Signal条件变量时的考虑
因为在signal线程中解锁互斥量mutex和发出唤醒信号condition_signal是两个单独的操作,所以就存在一个顺序的问题。谁先随后可能会产生不同的结果。如下:
(1) 按照 unlock(mutex); condition_signal()顺序, 当等待线程被唤醒时,因为mutex已经解锁,因此被唤醒的线程很容易就锁住了mutex然后从conditon_wait()中返回了。
(2) 按照 condition_signal(); unlock(mutext)顺序,当等待线程被唤醒时,它试图锁住mutex,但是如果此时mutex还未解锁,则线程又进入睡眠,mutex成功解锁后,此线程在再次被唤醒并锁住mutex,从而从condition_wait()中返回。
可以看到,按照(2)的顺序,对等待线程可能会发生2次的上下文切换,影响性能. 那使用(1)又是什么情况呢,在unlock之后,其他的线程就可以获取这个mutex进而修改了predicate,这样signal就失去了意义,也和我们的最初想法不一样。就我个人而言,我会选择使用(2)。