对条件变量以及其中的互斥量的理解


轮询程序

要让某个线程等待某个条件发生才继续执行,可以让线程用一个循环定时地轮询,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *fun(void *arg)
{
int *var_p = arg;
pthread_mutex_lock(&mutex);
while(*var_p == 0) {
pthread_mutex_unlock(&mutex);
usleep(100000);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
puts("ok");
}
int main()
{
int var = 0;
pthread_t pid;
pthread_create(&pid, NULL, fun, &var);
getchar();
pthread_mutex_lock(&mutex);
var = 1;
pthread_mutex_unlock(&mutex);
pthread_join(pid, NULL);
return 0;
}

这个程序大体上是这样的:

  • 子线程每隔100ms轮询一次,直到var不等于0的条件满足,才继续执行输出ok
  • 主线程在标准输入中获得输入后会将var的值设置为 1,相当于子线程等待的条件满足了。
  • 其中由于var是临界资源,子线程和主线程有竞争条件,所以加了互斥量去保护它。

条件变量程序

上面那个轮询程序不太靠谱,轮询间隔时间太短太消耗CPU资源,太长可能会错过条件的发生。

条件变量Condtion Variable)就可以很好地解决这个问题。利用条件变量可以让一个线程在条件不满足时阻塞,直到另一个线程在条件满足时才通知唤醒它。改造的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *fun(void *arg)
{
int *var_p = arg;
pthread_mutex_lock(&mutex);
while(*var_p == 0) {
pthread_cond_wait(&cond, &mutex); // wait
}
pthread_mutex_unlock(&mutex);
puts("ok");
}
int main()
{
int var = 0;
pthread_t pid;
pthread_create(&pid, NULL, fun, &var);
getchar();
pthread_mutex_lock(&mutex);
var = 1;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond); // signal
pthread_join(pid, NULL);
return 0;
}

条件变量中pthread_cond_wait()函数用来阻塞线程,而pthread_cond_signal()pthread_cond_broadcast())函数用来唤醒等待的线程,这几个函数的原型如下:

1
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);

1
int pthread_cond_signal(pthread_cond_t *cond);

1
int pthread_cond_broadcast(pthread_cond_t *cond);

下面会对几个问题说明我的一些理解。


几个问题

问题一:pthread_cond_wait()函数为什么需要传递一个互斥量参数

https://www.zhihu.com/question/24116967/

首先需要明确的是,和上面那个轮询程序一样,这个互斥量参数是用来保护var这个所要等待的条件的,是需要这个互斥量的。

pthread_cond_wait()在调用过程中做的事情如下:

  1. 把当前线程放入cond中的等待唤醒的线程列表中
  2. 释放mutex的锁
  3. 阻塞,直到另一个线程调用pthread_cond_signal()pthread_cond_broadcast()
  4. 在返回前重新获得mutex的锁

第2步第4步对于mutex的解锁加锁动作就相当于上面那个轮询程序的第12行第14行

如果pthread_cond_wait()函数不传递这个互斥量参数,那么解锁互斥量只能等到线程阻塞被唤醒从pthread_cond_wait()函数返回,而唤醒又需要条件改变调用pthread_cond_signal()函数,条件改变的前提是获得那个保护条件的互斥量的锁,而互斥量解锁又得等到阻塞的线程被唤醒。。简而言之,死锁了。所以需要传递它,使得互斥量的解锁在函数内进行。

在网上对于pthread_cond_wait()为什么需要mutex参数的观点还有:同步pthread_cond_wait()函数和pthread_cond_signal()函数,避免pthread_cond_wait()函数调用后还未放入等待列表时,另一个线程条件满足调用pthread_cond_signal()函数,从而错过了这次条件的变化。

一开始我也是这么理解的,不过现在我觉得这不是“需要”mutex参数的原因,而是一个作用。我相信确实起到了同步pthread_cond_wait()函数和pthread_cond_signal()函数的作用。

因为如果只是单纯如上所述,为什么第2步释放锁的动作不放到第1步去,一开始就解锁应该会更好因为提高了并发的粒度。

此外而APUE里有提到:

The caller passes it locked to the function, which then atomically places the calling thread on the list of threads waiting for the condition and unlocks the mutex. This closes the window between the time that the condition is checked and the time that the thread goes to sleep waiting for the condition to change, so that the thread doesn’t miss a change in the condition.

大概意思应该是,条件检查->线程放入队列->互斥量解锁->阻塞休眠,利用互斥量将前三步绑在一起,使得从条件检查开始到阻塞休眠之前不会被干扰。


问题二:为什么使用while循环进行条件检查

在轮询程序用while是为了轮询,而改用条件变量还用while而不是if,其作用是:

  1. pthread_cond_wait()函数返回后,由于其他线程的抢占有可能条件又改变了,变成不成立,此时应该要重新一下判断条件,而用while循环就能达成这个目的。

  2. 避免“虚假唤醒”,即条件没有成立却调用了pthread_cond_signal()函数。