互斥量用于防止多个线程同时访问同一共享变量。条件变量允许一个线程就某个共享变量(或其他共享资源)的状态变化通知其他线程,并让其他线程等待(堵塞于)这一通知。

下列代码是未使用条件变量的例子,它假设由若干个线程生成一些“产品单元”,供主线程消费。还使用了一个由互斥量保护的变量avail来代表待消费产品的数量:

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;static int avail = 0;

生产者线程:

s = pthread_mutex_lock(&mtx);if(s!=0)    errExitN(s, "pthread_mutex_lock");avail++;s = pthread_mutex_unlock(&mtx);if(s!=0)    errExitN(s, "pthread_mutex_unlock");

主线程(消费者):

for( ; ; ){    s = pthread_mutex_lock(&mtx);    if(s!=0)        errExitN(s, "pthread_mutex_lock");    while(avail > 0){        avail--;    }    s = pthread_mutex_unlock(&mtx);    if(s!=0)        errExitN(s, "pthread_mutex_unlock");}

上述代码虽然可行,但由于主线程不停地循环检查变量avail的状态,故而造成CPU资源的浪费。
采用了条件变量(condition variavle),这样问题就迎刃而解:允许一个线程休眠(等待)直至接获另一线程的通知(收到信号)去执行某些操作(例如,出现一些“情况”后,等待者必须立即做出响应)。

  • 条件变量总是结合互斥量使用。
  • 条件变量就共享变量的状态改变发出通知,而互斥量则提供对该共享变量访问的互斥(mutual exclusion)。

这里使用的术语“信号”(signal),与系统的信号机制(signal)无关,而是发出信号的意思。

由静态分配的条件变量

如同互斥量一样,条件变量的分配,由静态和动态之分。
这里先讨论静态分配。
条件变量的数据类型是pthread_cond_t
类似于互斥量,使用条件变量前必须对其初始化。
对于经由静态分配的条件变量,将其赋值为PTHREAD_COND_INITIALIZER即完成初始化操作。
例如:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

依据SUSv3规定,将后续所描述的操作施之于一个条件变量的副本(copy)时,其结果未定义。所有操作仅能针对条件变量的原本执行,要么经由PTHREAD_COND_INITIALIZER进行了静态初始化,要么使用pthread_cond_init()做了动态初始化处理。

通知和等待条件变量

条件变量的主要操作是发送信号(signal)和等待(wait)。发送信号操作即通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变。等待操作是指在收到一个通知前一直处于阻塞状态。

  • 函数pthread_cond_signal()pthread_cond_broadcast()均可针对由参数cond所指定的条件变量而发送信号。
  • pthread_cond_wait()函数将阻塞一线程,直至收到条件变量cond的通知。
#include int pthread_cond_signal(pthread_cond_t *cond);int pthread_cond_broadcast(pthread_cond_t *cond);int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);        All return 0 on success, or a positive error number on error.

函数pthread_cond_signal()pthread_cond_broadcast()之间的差别在于,二者对阻塞于pthread_cond_wait()的多个线程处理方式不同。pthread_cond_signal()函数只保证唤醒至少一条遭到阻塞的线程,而pthread_cond_broadcast()则会唤醒所有遭阻塞的线程。

使用函数pthread_cond_broadcast()总能产生正确结果(因为所有线程应都能处理多余和虚假的唤醒动作),但函数pthread_cond_signal()会更高效。

不过,只有当仅需唤醒一条(且无论是其中哪条)等待线程来处理共享变量的状态变化时,才应使用pthread_cond_signal()应用这种方式的典型情况是,所有等待线程都在执行完全相同的任务。

基于这些假设,函数pthread_cond_signal()会比pthread_cond_broadcast()更具效率,因为这可以避免发生如下情况:

  1. 同时唤醒所有等待线程。
  2. 某一线程首先获得调度。此线程检查了共享变量的状态(在相关互斥量的保护之下),发现还有发任务需要完成。该线程执行了所需工作,并改变共享变量状态,以表明任务完成,最后释放对相关互斥量的锁定。
  3. 剩余的每个线程轮流锁定互斥量并检测共享变量的状态。不过,由于第一个线程所做的工作,余下的线程发现无事可做,随即解锁互斥量转而休眠(即再次调用pthread_cond_wait())。

相形之下,函数pthread_cond_broadcast()所处理的情况是:处于等待状态的所有线程执行的任务不同(即各线程关联于条件变量的判定条件不同)

条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。
发送信号时若无任何线程在等待该条件变量,这个信号也就会不了了之。线程如在此后等待该条件变量,只有当再次收到此变量的下一信号时,方可解除阻塞状态。

  • 函数pthread_cond_timedwait()与函数pthread_cond_wait()几近相同,唯一区别在于,由参数abstime来指定一个线程等待条件变量通知时休眠时间的上限。
#include int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);        Returns 0 on success, or a positive error number on error.

参数abstime是一个timespec类型的结构,用以指定自Epoch 以来以秒和纳秒(nanosecond)为单位表示的绝对(absolute)时间。如果abstime指定的时间间隔到期且无相关条件变量的通知,则返回ETIMEOUT错误。

在生产者-消费者示例中使用条件变量

下面对前面的示例做出修改,引入条件变量。对全局变量、相关互斥量以及条件变量的声明代码如下:

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;static pthread_cond_t  cond = PTHREAD_COND_INITIZLIZER;static int avail = 0;

除了增加对函数pthread_cond_signal()的调用外,生产者线程的代码与之前并无变化:

s = pthread_mutex_lock(&mtx);if(s != 0)    errExit(s, "pthread_mutex_lock");avail ++;s = pthread_mutex_unlock(&mtx);if(s != 0)    errExitEN(s, "pthread_mutex_unlock");    s = pthread_cond_signal(&cond);if(s != 0)    errExitEN(s, "pthread_cond_signal");

在分析消费者代码之前,需要对pthread_cond_wait()函数做更为详细的解释。前文已经指出,条件变量总是要与一个互斥量相关。将这些对象通过函数参数传递给pthread_cond_wait(),后者执行如下操作步骤。

  • 解锁互斥量mutex。
  • 堵塞调用线程,直至另一线程就条件变量cond发出信号。
  • 重新锁定mutex。

设计pthread_cond_wait()执行上述步骤,是因为通常情况下,代码会以如下方式访问共享变量:

s = pthread_mutex_lock(&mtx);if(s != 0)    errExitEN(s, "pthread_mutex_lock");    while(/* Check that shared variable is not in state we want */)    pthread_cond_wait(&cond, &mtx);    /* Now shared variable is in desired state; do some work */s = pthread_mutex_unlock(&mtx);if(s != 0)    errExitEN(s, "pthread_mutex_unlock");

后面会介绍如何将pthread_cond_wait()置于while循环中,而非if语句中。

在以上代码中,两处对共享变量的访问都必须置于互斥量的保护之下,其原因之前已解释,换言之,条件变量与互斥量之间存在天然的关联关系

  1. 线程在准备检查共享变量状态时锁定互斥量。
  2. 检查共享变量的状态。
  3. 如果共享变量未处于预期状态,线程应在等待条件变量并进入休眠前解锁互斥量(以便其他线程能访问该共享变量)。
  4. 当线程因为条件变量的通知而被再度唤醒时,必须对互斥量再次加锁,因为在典型情况下,线程会立即访问共享变量。

函数pthread_cond_wait()会自动执行最后两步中对互斥量的解锁和加锁动作。第3步中互斥量的释放与陷入对条件变量的等待同属于一个原子操作。换句话说,在函数pthread_cond_wait()的调用线程陷入对条件变量的等待之前,其他线程不可能获取到该互斥量,也不可能就该条件变量发出信号。

通过观察得出推论:条件变量与互斥量之间存在天然关系,同时等待相同条件变量的所有线程在调用pthread_cond_wait()pthread_cond_timedwait()时,必须指定同一互斥量。实际上,pthread_cond_wait()在调用期间能将条件变量与一个唯一的互斥量做动态绑定。SUSv3规定,在针对同一条件变量并发调用pthread_cond_wait()时,若使用多个互斥量会导致未定义的结果。

  • 未完待办