互斥量用于防止多个线程同时访问同一共享变量。条件变量允许一个线程就某个共享变量(或其他共享资源)的状态变化通知其他线程,并让其他线程等待(堵塞于)这一通知。
下列代码是未使用条件变量的例子,它假设由若干个线程生成一些“产品单元”,供主线程消费。还使用了一个由互斥量保护的变量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 <pthread.h>
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()
更具效率,因为这可以避免发生如下情况:
- 同时唤醒所有等待线程。
- 某一线程首先获得调度。此线程检查了共享变量的状态(在相关互斥量的保护之下),发现还有发任务需要完成。该线程执行了所需工作,并改变共享变量状态,以表明任务完成,最后释放对相关互斥量的锁定。
- 剩余的每个线程轮流锁定互斥量并检测共享变量的状态。不过,由于第一个线程所做的工作,余下的线程发现无事可做,随即解锁互斥量转而休眠(即再次调用
pthread_cond_wait()
)。
相形之下,函数pthread_cond_broadcast()
所处理的情况是:处于等待状态的所有线程执行的任务不同(即各线程关联于条件变量的判定条件不同)。
条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。
发送信号时若无任何线程在等待该条件变量,这个信号也就会不了了之。线程如在此后等待该条件变量,只有当再次收到此变量的下一信号时,方可解除阻塞状态。
- 函数
pthread_cond_timedwait()
与函数pthread_cond_wait()
几近相同,唯一区别在于,由参数abstime
来指定一个线程等待条件变量通知时休眠时间的上限。
#include <pthread.h>
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语句中。
在以上代码中,两处对共享变量的访问都必须置于互斥量的保护之下,其原因之前已解释,换言之,条件变量与互斥量之间存在天然的关联关系。
- 线程在准备检查共享变量状态时锁定互斥量。
- 检查共享变量的状态。
- 如果共享变量未处于预期状态,线程应在等待条件变量并进入休眠前解锁互斥量(以便其他线程能访问该共享变量)。
- 当线程因为条件变量的通知而被再度唤醒时,必须对互斥量再次加锁,因为在典型情况下,线程会立即访问共享变量。
函数pthread_cond_wait()
会自动执行最后两步中对互斥量的解锁和加锁动作。第3步中互斥量的释放与陷入对条件变量的等待同属于一个原子操作。换句话说,在函数pthread_cond_wait()
的调用线程陷入对条件变量的等待之前,其他线程不可能获取到该互斥量,也不可能就该条件变量发出信号。
通过观察得出推论:条件变量与互斥量之间存在天然关系,同时等待相同条件变量的所有线程在调用
pthread_cond_wait()
或pthread_cond_timedwait()
时,必须指定同一互斥量。实际上,pthread_cond_wait()
在调用期间能将条件变量与一个唯一的互斥量做动态绑定。SUSv3规定,在针对同一条件变量并发调用pthread_cond_wait()
时,若使用多个互斥量会导致未定义的结果。
- 未完待办