一、本文目的
首先说明,本文重点不在怎么用条件变量。这里我先列出 apue 中对于pthread_cond_wait函数的这么一段话:
调用者把锁住的互斥量传给函数,函数然后自动把调用线程放到等待条件的线程列表上,**对互斥量解锁。**这就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。
这段话的信息量很大,其中关于互斥量的操作可以理解为以下三个点:
- 调用 pthread_cond_wait 前需要先对互斥量 mutex 上锁,才能把 &mutex 传入 pthread_cond_wait 函数。
- 在 pthread_cond_wait 函数内部,会首先对传入的 mutex 解锁。
- 当等待的条件到来后,pthread_cond_wait 函数内部在返回前会去锁住传入的 mutex 。
我当时看到这里,各种疑问,传入前为何要锁,传入后为何要释放,返回时又为何再次锁?
本文就这三个问题进行详细解释。不过在此之前,我们需要了解为什么要有条件变量。即条件变量的作用。
二、为何需要条件变量
如果没有条件变量,那么我们等待一个条件满足则会是下面这样的模型:
首先加锁进入临界区去查看条件是否满足,不满足则解锁离开临界区,睡眠一段时间再继续循环判断。在这种情况下如果刚离开临界区,条件变为满足,那么线程必须还要等一段时间重新进入临界区才能知道条件满足(如果在这段时间内,条件依旧一直保持满足的话),如果这一小段时间条件又变为不满足,那么这个线程还要继续循环判断。、
不断地加锁解锁(会影响使用同一把锁的其他线程),还不能第一时间收到条件满足。这种模型既费时又开销大。
所以条件变量的产生,正是为了不循环加锁解锁,并且第一时间收到条件满足的通知。
三、三个问题
要回答那三个问题,那么首先需要明白等待与唤醒的配合。
下图是我参考其他人的图(原图有误)更正后所画的。其实这个图就能解释那三个问题:pthread_cond_wait 传入前为何要锁,传入后为何先解锁,以及返回前为何再锁。不过我还是详细解释一下。
图中有一个关键点,就是判断条件是否满足,是在调用 pthread_cond_wait 之前,上锁之后,就是说 pthread_cond_wait 不具备判断条件的能力,需要我们在外部写判断语句。
条件不满足时,才会进入 pthread_cond_wait 。
进入 pthread_cond_wait 先解锁就马上阻塞。
pthread_cond_signal 唤醒的是阻塞在 pthread_cond_wait 的进程。
可以结合下面的代码会更清楚。
以下 pthread_cond_wait 和 pthread_cond_signal 的通常用法的伪代码(条件为:value 是不是大于 0:
lock(&mutex);
while ( value <= 0) //需要 value > 0, 所以 value <= 0就条件不满足
{
pthread_cond_wait(&cond, &mutex);
//条件满足,进行相关处理。
}
unlock(&mutex);
lock(&mutex);
if (value == 0)
{
value++;
}
if (value > 0)
{
pthread_cond_signal(&cond);
}
unlock(&mutex);
把这个基本流程弄清楚后,就可以解释那三个问题了。
四、传入前锁 mutex
为了方便大家观看,每个问题的解释我都会再次把上图贴出。
传入前,锁 mutex 是为了保证线程从条件判断到进入 pthread_cond_wait 前,条件不被改变。
如果没有传入前的锁,就会有这样的情况:线程 A 判断条件不满足之后,调用 pthread_cond_wait 之前,A 因为休眠,或者因为多线程下,多个线程执行顺序和快慢的因素,令线程 B 更改了条件,使得条件满足。但此时线程 A 还没有调用 pthread_cond_wait。等到线程 A 又启动调用 pthread_cond_wait 后虽然条件满足,但却收不到 pthread_cond_signal 的唤醒,就一直阻塞下去。
五、传入后解锁 mutex
传入后解锁是为了条件能够被改变。
传入后的解锁,是因为调用 pthread_cond_signal 的那部分,需要先加锁更改条件后才调用pthread_cond_signal。(更改条件与等待条件满足,都是针对条件这一个资源的竞争,所以调用 pthread_cond_wait 和调用 pthread_cond_signal 的两个线程需要同一把锁)
如果 pthread_cond_wait 内不对 mutex 解锁,那么在调用 pthread_cond_wait 后,其他线程就不能更改条件,条件就会一直不满足。
六、返回前再次锁 mutex
-
返回前再次锁 mutex 是为了保证线程从 pthread_cond_wait 返回后到再次条件判断前不被改变。
-
保证在 pthread_cond_signal 之后与解锁 mutex 之间可能需要的其他语句能够执行。
对于 1,这里的理由与传入 pthread_cond_wait 前锁 mutex 的理由差不多。如果不锁,那么线程 A 调用 pthread_cond_wait 后,条件满足,线程 A 被唤醒,从 pthread_cond_wait 返回。线程 B 在此时更改了条件,使得条件不满足。线程 A 并不知道条件又被更改,还是以为条件满足,就可能出错。
对于 2,只要在 pthread_cond_signal 之后与解锁 mutex 之间有其他语句需要执行,那么由于 mutex 在这时已经被这个线程锁,还没有解锁,所以调用 pthread_cond_wait 的那个线程在pthread_cond_wait 返回前的锁 mutex 的行为就会阻塞,直到 pthread_cond_signal 后的语句执行完解锁,pthread_cond_wait 才会返回。
说到这里就顺便说一下,由于 pthread_cond_wait 返回再次锁的行为,pthread_cond_signal 不一定放在 lock() 和 unlock() 中间。
pthread_cond_signal 的两种写法
lock(&mutex);
//一些操作
pthread_cond_signal(&cond);
//一些操作
unlock(&mutex);
缺点:在某些线程的实现中,会造成等待线程从内核中唤醒(由于 cond_signal)回到用户空间,然后 pthread_cond_wait 返回前需要加锁,但是发现锁没有被释放,又回到内核空间所以一来一回会有性能的问题。
但是在 LinuxThreads 或者 NPTL 里面,就不会有这个问题,因为在 Linux 线程中,有两个队列,分别是 cond_wait 队列和 mutex_lock 队列, cond_signal 只是让线程从 cond_wait 队列移到 mutex_lock队列,而不用返回到用户空间,不会有性能的损耗。所以 Linux 中这样用没问题。
lock(&mutex);
//一些操作
unlock(&mutex);
pthread_cond_signal(&cond);
优点:不会出现之前说的那个潜在的性能损耗,因为在 signal 之前就已经释放锁了。
缺点:如果 unlock 之后 signal 之前,发生进程交换,另一个进程(不是等待条件的进程)拿到这把梦寐以求的锁后加锁操作,那么等最终切换到等待条件的线程时锁被别人拿去还没归还,只能继续等待。
七、尾语
总的来说,条件变量带个锁的目的就是让等待条件成立的线程不会丢掉条件成立的情况。
以上
(SAW:Game Over!)