本章介绍线程用来同步彼此行为的两个工具:互斥量(mutexe)和条件变量(condition variable)。互斥量可以帮助线程同步对共享资源的使用,以防如下情况发生:线程甲试图访问一共享变量时,线程乙正在对其修改。条件变量则是在紫外的拾遗补缺,允许线程互相通知共享变量(或其他共享资源)的状态发生了变化。
30.1保护对共享变量的访问:互斥量
线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程 不会同时修改同一变量,或者某一线程不会读取正有其他线程修改的变量。术语临界区(critical section)是指访问同一共享资源的代码片段,并且这段代码的执行应为原子操作,亦即,同时访问统一共享资源的其他线程应该中断该片段的执行。
程序清单30-1中的简单示例,展示了以分源自方式访问共享资源时发生的问题。该程序创建了两个线程,且均执行同一函数。该函数执行一个循环,重复以下步骤:将glob复制到本地变量loc中,然后递增loc再把loc复制回glob.以此不断增加全局变量glob的值。因为loc是分配与线程栈中的自动变量,所以每个线程都有一份。循环重复的次数要么由命令行参数指定。要么取默认值。
程序清单30-1:两线程一错误方式递增全局变量的值
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
static volatile int glob = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static void * /* Loop 'arg' times incrementing 'glob' */
threadFunc(void *arg)
{
int loops = *((int *) arg);
int loc, j, s;
for (j = 0; j < loops; j++) {
loc = glob;
loc++;
glob = loc;
}
return NULL;
}
int
main(int argc, char *argv[])
{
pthread_t t1, t2;
int loops, s;
loops = (argc > 1) ? atoi(argv[1]) : 10000000;
s = pthread_create(&t1, NULL, threadFunc, &loops);
if (s != 0){
printf("pthread_create1\n");
return -1;
}
s = pthread_create(&t2, NULL, threadFunc, &loops);
if (s != 0){
printf( "pthread_create\n");
return -1;
}
s = pthread_join(t1, NULL);
if (s != 0){
printf( "pthread_join1\n");
return -1;
}
s = pthread_join(t2, NULL);
if (s != 0){
printf("pthread_join\n");
return -1;
}
printf("glob = %d\n", glob);
exit(EXIT_SUCCESS);
}
运行程序清单30-1中的示例,并指定每个线程均针对该变量递增1000次,看起来一切正常。
不过很有可能会发生如下情况:在线程已尚未得以运行时,线程甲已经执行完毕并且退出了。如果夹带每个线程的工作量,结果将完全不同。
执行到最后,glob值应该时2000万,问题的原因是由于如下的执行序列(参见图30-1)。
- 线程1将glob赋值给局部变量loc。假设blog当前值是两千。
- 线程1的时间片期满,线程2开始运行。
- 线程2执行多次循环:将全局变量的值置于局部变量loc,递增loc,再将结果写回变量glob。第一次循环时,glob的值为2000.假设线程2的时间片到期时glob的值已经增至3000.
- 线程1获得另一时间片,并从上次停止处运行,线程1在上次运行时,已将glob的值(2000)复制给loc现在递增loc再将loc的值2001xiehuiglob.此时,线程2此前操作的而结果遭到覆盖。
- 如果使用同样的命令行参数将该程序运行多洗,glob的值会波动很大:
这一行为的不确定性,实应归咎于内核CPU调度决定的难以预见。若在复杂程序中发生这一不确定欣慰,则意味着着此类陈旭偶尔出错,难以重现,因此也很难发现。
使用如下语句,将程序清单30-1中函数threadFunc()内佛尔循环中的の条语句加以替换,似乎可以解决这个问题:
glob++; /*or :++glob*/
不过在很多引荐架构上(例如RISC系统),编译器依然会将这条语句转换成机器码,其执行步骤仍就等同于threadFunc循环内的三条语句。换言之,尽管C语言的递增看似简单,其操作也未必就属于原子操作,依然可以发生上述行为。
为避免线程更新共享变量时所出现的问题,必须使用互斥量(mutex是 mutualexclusion的缩写)来确保同时仅有一个线程可以访问某项资源。更为全面的说法是,可以使用互斥量来保证遂人意共享资源的原子访问,而保护共享变量是最常见的用法。
互斥量有两种状态:已锁定和未锁定。任何时候之多有一个线程可以锁定该互斥量。试图对已锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时的使用的方法。
一旦线程锁定互斥量,所继承为互斥量的所有者。只有所有者才能给互斥量解锁。这一属性改善了使用互斥量的代码结构,以估计到对互斥量实现的优化。因为所有权的关系,有时会使用术语获取(acquire)和释放(release)来替代加锁和解锁。
一般情况下,对每一共享资源(可能由多个相关变量组成)会使用托尼盖的互斥量,每一个线程在访问同一资源时将采用如下协议。
- 针对共享资源锁定互斥量
- 访问共享资源
- 对互斥量解锁。
如果多个线程试图执行这一代码块(一个临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭到阻塞),及同事只有一个线程能够进到这段代码区域,如图30-2所示:
最后请注意,使用互斥锁仅是一种建议,而非强制。亦即可以考虑不适用互斥量而进访问相应的共享变量。为了安全地处理共享变量,所有线程在使用互斥量时必须互相协调,遵守既定的锁定规则。
30.1.1 静态分配的互斥量
互斥量既可以想静态变量那样分配,也可以在运行时动态创建(例如,通过malloc()在一块内存中分配)。动态互斥量的创建稍微有些复杂,将延后至30.1.5节再做讨论。
互斥量是属于pthread_mutex_t类型的变量
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
Both return 0 on success,or a positive error number on error
量。在使用之前必须对其初始化。对于静态分配的互斥量而言,可如下例所示,将PTHREAD_MUTEX_INITIALIZER赋给互斥量。
pthread_mutex_t mtx = PTHREAD_MUTEXT_INITIALIZER;
依照SUSv3规定,对某一互斥量的副本(copy)执行本节(30.1节)后续所描述的操作将导致未定义的结果。此类操作只能施之于如下两类互斥量的“真身”,经由PTHREAD_MUTEX_INITIALIZER初始化的静态互斥量或者经由pthread_mutex_init()初始化的动态互斥量。
30.1.2 加锁和解锁互斥量
初始化之后,互斥量处于未锁定状态。函数pthread_mutex_lock()可以锁定某一互斥量,而函数pthread_mutex_unclock()则可以将一个互斥量解锁。
要锁定互斥量,在调用pthread_mutex_lock()时需要指定互斥量。如果互斥量当前处于为锁定状态,该调用将锁定互斥量并立即返回,如果其他线程已经锁定了这一互斥量,那么pthread_mutex_lock()调用会一直阻塞,直至该互斥量被解锁,到那时,调用将锁定互斥量并返回。
如果发起pthread_mutex_lock()调用的线程自身之前依然将目标互斥量锁定,对于互斥量的默认类型而言,可能会产生两种后果--视具体实现而定:线程陷入死锁(deadlock),饮食如锁定已为自己所持有的互斥量而遭到阻塞;或者调用失败,返回EDEADLK错误。在linux上默认情况下线程会发生死锁。
函数pthread_mutex_unlock()将解锁之前已遭调用线程锁定的互斥量。以下行为均属错误:对处于为锁定状态的互斥量进行解锁,或者解锁由其他线程锁定的互斥量。
如果有不止一个线程在等待获取由函数pthread_mutex_unlock()解锁的互斥量,则无法判断哪个线程如愿以偿。
示例程序
程序清单30-2是对程序清单30-1的修改,使用了一个互斥量来保护对全局变量glob的访问。使用与之前类似的命令行来运行这个改版程序,可以看到对glob累加总是能保持正确。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
static volatile int glob = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static void * /* Loop 'arg' times incrementing 'glob' */
threadFunc(void *arg)
{
int loops = *((int *) arg);
int loc, j, s;
for (j = 0; j < loops; j++) {
s = pthread_mutex_lock(&mtx);
if (s != 0){
printf("pthread_mutex_lock\n");
return NULL;
}
loc = glob;
loc++;
glob = loc;
s = pthread_mutex_unlock(&mtx);
if (s != 0){
printf("pthread_mutex_unlock\n");
return NULL;
}
}
return NULL;
}
int
main(int argc, char *argv[])
{
pthread_t t1, t2;
int loops, s;
loops = (argc > 1) ? atoi(argv[1]) : 10000000;
s = pthread_create(&t1, NULL, threadFunc, &loops);
if (s != 0){
printf("pthread_create1\n");
return -1;
}
s = pthread_create(&t2, NULL, threadFunc, &loops);
if (s != 0){
printf( "pthread_create\n");
return -1;
}
s = pthread_join(t1, NULL);
if (s != 0){
printf( "pthread_join1\n");
return -1;
}
s = pthread_join(t2, NULL);
if (s != 0){
printf("pthread_join\n");
return -1;
}
printf("glob = %d\n", glob);
exit(EXIT_SUCCESS);
}
pthread_mutex_trylock()和pthread_mutex_timedlock()
Pthreads API 提供了pthread_mutex_lock()函数的两个变体pthread_mutex_trylock()和pthread_mutex_timedlock(),可参考手册页获取这些函数的原型。
如果信号量已经锁定,对其执行函数pthread_mutex_trylock()会失败并返回EBUY错误,除此之外,该函数与pthread_mutex_lock()行为相同。
除了调用者可以指定一个附加参数abstime(设置线程等待获取互斥量时休眠的时间限制)外,函数pthread_mutex_timedlock()与pthread_mutex_lock()没有差别。如果参数abstime指定的时间间隔期满,而调用线程有没有获得对互斥量的所有权,那么函数pthread_mutex_timedlock()返回ETIMEDOUT错误。
函数pthread_mutex_trylock()和pthread_mutex_timedlock()比pthread_mutex_lock()的使用频率要低很多,在大多数经过良好设计的应用程序中,线程对互斥量的持有时间尽可能短,以避免妨碍其他线程的并发执行。这样保证了早堵塞的其他线程可以很快获取对互斥量的锁定。若某一线程使用pthread_mutex_trylock周期性的轮询是否可以对互斥量枷锁,则有可能要承担性和杨的风险:当队列中的其他线程通过调用pthread_mutex_lock()相继获得对互斥量的访问时,该线程将时钟与此互斥量无缘。
30.1.3互斥量的性能
使用互斥量的开销多大?前面已经展示了递增共享变量程序的两个不同版本:没有使用互斥量的程序清单30-1和使用互斥量的程序清单30-2。在下6-32架构的Linux2.6.31(含 NPTL)系统下运行这两个程序,如令单一线程循环1000万次,前者共花费了0.35秒(并产生错误结果),而后者需要3.1秒。
乍一看,代价极高。不过考虑一下前者(程序清单30-1)执行的主循环。在该版本中,函数threadFunc()于for循环中,先递增循环控制变量,再将其与另一变量进行比较,随后执行两个赋值操作和一个递增皂搓,最后返回循环起始处开始下一次循环。而后者---使用互斥量的版本(30-2)执行了相同步骤,不过在每次循环的前后多了加锁和解锁互斥量的工作。换言之,对互斥量的加锁和解锁的开销略低于第一个程序的10次循环操作。成本相对比较低廉。此外,在通常情况下,线程会花费更多时间去做其他工作,对互斥量的加锁和解锁操作相对少的多得多,因此使用互斥量对于大部分应用程序的性能并无显著影响。
进而言之,在系统上运行一些简单的测试程序,结果显示,如将使用函数fcntl()加锁、解锁一片文件区域的代码循环2000万次,需耗时44秒,而将对系统V信号量(semaphore)的递增和递减代码循环2000万次,则需要28秒,文件锁和信号量的问题在于,其锁定和解锁总是发起系统调用,而每个系统调用的开销虽小,但颇为可观。与之相反,互斥量的实现采用了机器语言级的原子操作(在内存中执行,对所有线程可见)。只有发生锁的争用时才会执行系统调用。
Linux上,互斥量的实现采用可futex(来自快速用户空间互斥量(fast user space mutex)的首字母缩写)。而对锁的争用则使用了futex()系统调用。本书无意描述futex。
30.1.4 互斥量的死锁
有时,一个线程需要同时访问两个或多个不同的共享资源,而每个资源又由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。图30-3展示了一个死锁的例子,其中每个线程都成功的所著一个互斥量,接着试图随已为另一线程锁定的互斥量加锁。两个线程将无限期的等下去。
要避免此类死锁问题,最简单的方法时=是定义互斥量的层级关系,当多个线程对异族互斥量操作时,总是应该以相同顺序对改组互斥量进行锁定。例如在图30-3所示场景中,如果两个线程总是先锁定mutex1在锁定mutex2,死锁就不会出现。有时,互斥量间的层级关系逻辑清晰。不过即便没有,依然可以设计出所有线程都必须遵循的强制层级顺序。
另一种方案的使用率较低,就是“尝试一下,然后恢复”。在这种方案中,线程先使用函数pthread_mutex_lock()锁定第一个互斥量 ,然后使用函数pthread_mutex_trylock()来锁定其余互斥量。如果任一pthread_mutex_try()调用失败(返回EBUSY,)那么该线程将释放所有互斥量,也许经过那一段时间间隔,从头再试,较之于按锁的层级关系来规避死锁,这种方法效率要低一些,因为可能要经历多次循环。另一方面,由于无需受制于严格的互斥量层级关系,该方法更为灵活。
30.1.5 动态初始化互斥量
静态初始值PTHREAD_MUTEX_INITIALIZER,只能用于对如下互斥量两进行初始化,:经由静态分配且携带默认属性。其他情况下必须调用pthread_mutex_init()对互斥量进行动态初始化。
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr);
Returns 0 on success,or a posive error number on error
参数mutex执行函数执行初始化操作的目标互斥量。参数attr是指向pthread_mutexattr_t类型对象指针,该对象在函数调用之前已经过了初始化处理,用于定义互斥量的属性。若将attr参数置为NULL,则该互斥量的各种属性会取默认值。
SUSv3规定,初始化一个业已初始化的互斥量将导致未定义的行为,应当避免这一行为。
在如下情况下,必须使用函数pthread_mutex_init(),而非使用静态化互斥量。
- 动态分配与队中的互斥量,例如动态创建针对某一结构的链表,表中每个结构都包含一个pthread_mutex_t来行的字段来存放互斥量,介意保护对该结构的访问。
- 互斥量是在栈中分配的自动变量。
- 初始化经由静态分配,且不使用默认属性值的互斥量。
当不在需要经由自动或动态分配的互斥量时,应使用pthread_mutex_destroy()将其销毁。(对于使用PTHREAD_MUTEX_INITIALIZER静态化的互斥量,无需调用pthread_mutex_destroy().)
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
Returns on success,or a positive error number on error
只有当互斥量处于未锁定状态,且后续业务任何线程企图锁定他时,将其销毁才是安全的。若互斥量主流与动态分配的一片内存区域中,应在释放(free)此内存区域前将其销毁。
经由pthread_mutex_destroy()销毁的互斥量,可调用pthread_mutex_init()对其重新初始化。
30.1.6 互斥量的属性
如前所述,可以在pthread_mutex_init()函数的arg参数中指定pthread_mutexattr_t类型对象,对互斥量的属性进行定义。通过pthread_mutexattr_t类型对象对互斥量属性进行初始化和读取操作的Pthreads函数有多个,本书不打算深入讨论互斥量属性的细节,也不会将初始化pthread_mutexattr_t对象属性的各种函数一一列出,下一节将会讨论互斥量的属性值一:类型。
30.1.7 互斥量类型
前面几页对互斥量的行为做了若干论述。
- 同一线程不应对同一互斥量加锁两次。
- 线程不应对不为自己所拥有的互斥量解锁(亦即,尚未锁定互斥量)。
- 线程不应对一尚未锁定的互斥量做解锁动作。
准确的说上述情况的结果取决于互斥量类型(type).SUSv3定义了以下互斥量类型。
PTHREAD_MUTEX_NORMAL
该类型的互斥量不具有死锁检测(自检)功能。如线程试图对已由自己锁定的互斥量加锁,则发生死锁。互斥量处于未锁定状态,或者已由其他线程锁定,对其解锁会导致不确定的结果。(在linux上,对这类互斥量的上述两种操作都会成功)。
PTHREAD_MUTEX_ERRORCHECK
对此类互斥量的所有操作都会执行错误检查。所有上述3中情况都会导致相关Pthreads函数返回错误。这类互斥量运行起来比一般类型要慢,不过可作为调试工具,以发现程序哪里违反了互斥量使用的基本原则。
PTHREAD_MUTEX_RECURSIVE
递归互斥量维护一个锁计数器当线程第一次取得互斥量时,会将锁计数器置1.后续由同一线程执行的每次加锁操作会递增锁计数器的数值,而枷锁操作则递减家属其技术。只有当锁计数器值降至0时,才会释放(release,亦即可为其他线程所用)该互斥量。解锁时如目标互斥量处于未锁定状态,或是由其他线程锁定,操作都会失败。
Linux的线程实现针对以上各种类型的互斥量提供了分标准的静态初始值(例如,PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP),以便那些通过静态分配的互斥量进行初始化,而无需使用pthread_mutex_init()函数。不过,为保证程序的可移植性,应该避免使用这些初始值。
除了上述类型,SUSv3还定义了PTHREAD_MUTEX_DEFAULT类型。使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量,或是经调用参数attr为NULL的pthread_mutex_init()函数所创建的互斥量,都属于此类型。置于该类型互斥量在本节开始处3个场景的行为,规范有意未定义,意在为互斥量的高效实现保留最大的灵活性。Linux上PTHREAD_MUTEX_DEFAULT类型的互相hi零的行为与PTHREAD_MUTEX_NURMAL类型相仿。程序清单30-3演示了如何设置互斥量类型,本例创建了一个带有错误检查属性的互斥量。
pthread_mutex_t mtx;
pthread_mytexattr_t mtxAttr;
int s,type;
s = pthread_mutexattr_init(&mtxAttr);
if(s!=0)
{
printf("pthread_mutexattr_init:");
return -1;
}
s = pthread_mutexattr_settype(&mtxAttr,PTHREAD_MUTEX_ERRORCHECK);
if(s!=0)
{
printf("pthread_mutexattr_settype:");
return -1;
}
s = pthread_mutex_init(mtx,&mtxAttr);
if(s!=0)
{
printf("pthread_mutex_init:");
return -1;
}
s = pthread_mutexattr_destroy(&mtxAttr);
if(s!=0)
{
printf("pthread_mutexattr_destroy:");
return -1;
}
30.2 通知状态的改变:条件变量(Condition Variable)
互斥量防止多个线程同时访问同一共享变量。条件变量允许一个线程就某个共享变量(或其他共享资源)的状态变化通知其他线程,比那个让其他线程等待(阻塞于)这一通知。
一个未使用条件变量的简单例子有助于展示条件变量的重要性。假设由若干线程生成一些“产品单元供主线程消费”,还使用了由互斥量保护的变量avail来代表待小消费产品的数量:
static pthread_mutex_t mtx =PTHREAD_MUTEX_INITIALZER;
static int avail = 0;
本节引用的代码片段摘自于随本书发布的源码文件、threads/prod_no_condvar.c
生产者线程的源代码如下:
//生产者线程的源代码如下
/*code to prodece a unit omitted*/
s = pthread_mutex_lock(&mtx);
if(s!=0)
{
printf("pthread_mutex_lock:");
return -1;
}
avail++; /*Let consumer know another unit is available*/
s=pthread_mutex_unlock(&mtx);
if(s!=0)
{
printf("pthread_mutex_unlock:");
return -1;
}
//主线程(消费者)的代码如下:
for(;;)
{
s = pthread_mutex_lock(&mtx);
if(s!=0)
{
printf("pthread_mutex_lock:");
return -1;
}
while(avail>0){ // consume all available units
//Do something with produced unit
avail--;
}
s=pthread_mutex_unlock(&mtx);
if(s!=0)
{
printf("pthread_mutex_unlock:");
return -1;
}
}
上述代码虽然可行,但由于主线程不停地循环价差avail的状态,故而造成CPU资源的浪费。采用了条件变量,这一问题就迎刃而解:允许一个线程休眠(等待)直至接获另一线程的通知(收到信号)去执行某些操作(如出现一些情况后,等待着必须立即做出响应)。
条件变量总是结合互斥量使用,条件变量就共享变量的状态发出通知。而互斥量则提供对共享变量访问的互斥。
30.2.1 由静态分配的条件变量
如同互斥量一样,条件变量的分配,有静态和动态之分。
条件变量的数据类型是pthread_cond_t.类似于互斥量,使用条件变量前必须对其初始化。对于经由静态分配的条件变量,将其赋值为PTHREAD_COND_INITIALIZER即完成初始化操作,可参考下面的例子:
pthread_cond)t cond = PTHREAD_INITIALIZER;
依据SUSv3规定,将本节后续所描述的操作施之于一个条件变量的副本,其结果未定义。所有操作仅能针对条件变量的原本执行,要么经由PTHREAD_COND_INITIALIZER进行了静态初始化,要么使用pthread_cond_init()做了动态初始化处理。
30.2.2通知和等待条件变量
条件变量的主要操作是发送信号(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_t *cond,pthread_mutex_t *mtx,
const struct timespec *abstime);
Returns 0 on success,or a positive error number on error
参数abstime是一个timespec类型的结构,用以指定自Epoch以来以秒和纳秒为单位标识的绝对时间,如果abstime指定的时间间隔到期且无相关条件变量的通知,则返回ETIMEOUT错误。
在生产者-消费者(producer-consumer)示例中使用条件变量
下面对前面的示例做修改,引入条件变量。对全局变量、相关互斥量以及条件变量的声明代码如下:
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITILAIZER;
static pthread_cond_t cond= PTHREAD_COND_INITILAIZER;
static int avail = 0;
本节中的代码片段摘自随本书发布的源码文件threads/prod_condvar.c
除了增加对函数pthread_cond_signal()调用外,生产者线程的代码与之前并无变化:
s = pthread_mutex_lock(&mtx);
if(s!=0)
{
printf("pthread_mutex_lock:");
return -1;
}
avail++; /*Let consumer know another unit is available*/
s=pthread_mutex_unlock(&mtx);
if(s!=0)
{
printf("pthread_mutex_unlock:");
return -1;
}
s = pthread_cond_signal(&cond) //wake sleeping consumer
if(s!=0)
{
printf("pthread_cond_signal:");
return -1;
}
在分析消费者代码之前,需要对pthread_cond_wait()函数做更为详细的解释,前文已经指出,条件变量总要与一个互斥量相关,将这些对象通过函数传递给pthread_cond_wait(),后者执行如下操作步骤。
- 解锁互斥量mutex。
- 堵塞调用线程,直至另一线程就条件变量cond发出信号。
- 重新锁定mutex。
设计pthread_cond_wait()执行上述步骤是因为通常情况下代码会以如下方式方位共享变量:
s = pthread_mutex_lock(&mtx);
if(s!=0)
{
printf("pthread_mutex_lock:");
return -1;
}
while(/*check that shared variable is not in state we want*/)
pthread_cond_wait(&cond,&mtx);
/*Now shared variable is in desired state; do dome work*/
s=pthread_mutex_unlock(&mtx);
if(s!=0)
{
printf("pthread_mutex_unlock:");
return -1;
}
下一节将会介绍为何将pthread_cond_wait()调用置于while循环中而非if语句中。
在以上代码中,两处对共享变量的访问都必须置于互斥量的保护之下,其原因之前已做了解释。换言之,条件变量与互斥量之间存在着天然的关联关系。
- 线程在准备检查共享变量状态时锁定互斥量。
- 检查共享变量的状态。
- 如果共享变量未处于预期咋黄台,线程应在等待条件变量进入休眠前后解锁互斥量(以便其他线程能访问该共享变量)。
- 当线程因为条件变量的通知而被再度唤醒时,必须对互斥量再次加锁,因为在典型情况下,线程会立即访问共享变量。
函数pthread_cond_wait()会自动执行最后两步中对互斥量的解锁和加锁动作。第三步中互斥量的释放与陷入对条件变量的等待属于同一个原子操作。换句话说,在函数pthread_cond_wait()调用线程陷入随条件变量的等待之前,其他线程不可能获取到该互斥量,也不可能就该条件变量发出信号。
通过观察得出推论:条件变量与互斥量之间存在天然关系,同时等待相同条件变量的所有线程在调用pthread_cond_wait()或pthread_cond_timedwait()时必须指定同一互斥量。实际上,pthread_cond_wait()在调用期间能将条件变量与一个唯一的互斥量做动态绑定。SUSv3规定,在针对同一条件变量并发调用pthread_cond_wait()时,若使用多个互斥量回到导致未定义的结果
结合以上所有细节,使用pthread_cond_wait()修改主(消费者)线程的代码如下:
for(;;)
{
s = pthread_mutex_lock(&mtx);
if(s!=0)
{
printf("pthread_mutex_lock:");
return -1;
}
while(avail == 0)
{
s = pthread_cond_wait(&cond,&mtx);
if(s!=0)
{
printf("pthread_mutex_lock:");
return -1;
}
}
while(avail>0){ // consume all available units
//Do something with produced unit
avail--;
}
s=pthread_mutex_unlock(&mtx);
if(s!=0)
{
printf("pthread_mutex_unlock:");
return -1;
}
//Perhaps do other work here that doesn't require mutex lock
}
最后再看一下pthread_cond_signal()和pthread_cond_broadcast()的使用,前面展示的生产者代码先调用了pthread_mutex_unlock()接着调用了pthread_cond_signal();换言之,先解锁与共享变量相关的互斥量,再就对应的条件变量发出信号。也可以将这两部颠倒执行,SUSv3允许以任一顺序执行这两个调用。
[Butenhof,1996]指出,在某些实现中,先解锁互斥量在通知条件变量可能比反序执行效率要高,如果仅在发出条件变量信号后才姐夫哦互斥量,执行pthread_cond_wait()调用的线程可能会在互斥量仍处于加锁状态时就醒来,当其发现互斥量仍未解锁,会立即再次休眠,这将会导致两个对于的上下文切换。有些实现运用等待变形技术解决了这一问题:将等待接受限号的线程从条件变量的等待队列转移至互斥量等待队列。这样,即便互斥量处于加锁状态,也无需切换上下文。
30.2.3 测试条件变量的判断条件(predocate)
每个条件变量都有与之相关的判断条件,涉及一个或多个共享变量。例如,在上一节的代码中,与xond相关的判断是(avail==0)。这段代码展示了一个通用的设计原则:必须要一个while循环,而不是if语句,来控制对pthread_cond_wait()的调用。这是因为,当代码从pthread_cond_wait()返回时,并不能确定判断条件的状态,所以应该立即重新判断检查条件,在条件不满足的情况下继续休眠等待。
从pthread_cond_wait()返回时,之所以不能对判断条件的状态做任何假设,其理由如下。
- 其他线程可能会率先醒来。也许有多个线程在等待获取与条件变量相关的互斥量。即使就互斥量发出通知的线程将判断条件置为预期状态,其他线程依然有可能率先获取互斥量并改变共享变量的状态,进而改变判断条件的状态。
- 设计时设置“宽松的”判断条件或许更为见到那。有时,用条件变量来表征可能性而非确定性,在设计应用程序时更为简单。换言之,就条件变量发出信号意味着“可能有些事情”需要接受信号的线程去响应,而不是“一定有一些事情”要做。使用这种方法,可以基于判断条件的近似情况来发送条件变量通知,接受信号的线程可以通过再次检查判断条件来确定是否真的需要做些什么。
- 可能会发生虚假唤醒的情况。在一些实现中,即使没有任何其他线程真的就条件变量发出信号,等待此条件变量的线程仍有可能醒来。在一些多处理器上,为确保高效实现而采用的技术会导致此类虚假唤醒。SUSv3对此明确认可
30.2.4示例程序:连接任意已终止线程
前面依然提及,使用pthread_join()只能连接一个指定线程,且该函数也未提供任何机制去链接任意已终止线程。本节展示如何使用条件变量绕过这一限制。
程序清单30-4为其每个命令行参数创建一个线程,每个线程休眠一段时间后随即退出,休眠时间由相应命令行参数所指定的秒数决定。这里用休眠间隔来模拟线程工作了一段时间。
该程序维护有一组全局变量,记录了所有一创建线程的信息。对于每个线程,全局数组thread中都含有一元素记录其线程ID(字段tid)以及当前状态(字段state)。状态字段state可设置为以下值:TS_ALIVE,表示线程是活动的;TS_TERMINATED,代表线程已经终结但尚未连接;TS_JOINED,表示线程终止且已被连接。
当线程终止时,将TS_TERMINATED赋给数组thread中对应元素的state字段,对表征已终止但尚未连接线程的全局计数器(numUnjoined)加一,并就条件变量threadDied发出信号。
主线程使用循环不断等待条件变量threadDied。当收到threadDied信号,且存在终止线程尚未被连接时,主线程将扫描thread数组,寻找为TS_TERMINATED的数组元素。对于处于该状态的每个线程,以数组thread中对应的tid字段调用pthread_join()函数,并肩state置为TS_JOINED。当主线程创建的所有线程终止时,及全局变量numLive值为0时,主循环结束。
以下shell会话日志展示了对程序清单30-4中程序的调用:
最后要指出,虽然示例中的线程都创建为处于可连接窗台,且终止后即由pthread_join()予以捕获,其实无需采用这一方法来发现线程的终止。可以将线程置为分离态(detached),无需使用pthread_join(),简单的利用thread数组(及其他相关全局变量)作为记录每个线程的终结的手段。
程序清单30-4;可以连接任意已终止的主线程。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
static pthread_cond_t threadDied = PTHREAD_COND_INITIALIZER;
static pthread_mutex_t threadMutex = PTHREAD_MUTEX_INITIALIZER;
/*Protects all of the following global variables*/
static int totThreads =0; /*Total number threads created*/
static int numLive = 0; /*Total number threads still alive or terminated
but not yet joined*/
static int numUnjoined = 0; /*Number of terminatedthreads that
have not yet been joined*/
enum tstate{ /*Thread states*/
TS_ALIVE, /*Thread is alive*/
TS_TERMINATED, /*Thread terminated,not yet joined*/
TS_JOINED /*Thread terminated,and joined*/
};
static struct{ /*Info about each thread*/
pthread_t tid; /*ID of this thread*/
enum tstate state; /*Thread state (TS_*- constats above)*/
int sleepTime; /*Number seconds to live before termination*/
}*thread;
static void *threadFunc(void *arg)
{
int idx = *((int*)arg);
int s;
sleep(thread[idx].sleepTime); /*simulate doing some work*/
printf("Thread %d terminating\n",idx);
s = pthread_mutex_lock(&threadMutex);
if(s!=0)
{
printf("pthread_mutex_lock:");
return NULL;
}
numUnjoined++;
thread[idx].state = TS_TERMINATED;
s=pthread_mutex_unlock(&threadMutex);
if(s!=0)
{
printf("pthread_mutex_unlock:");
return NULL;
}
s = pthread_cond_signal(&threadDied); //wake sleeping consumer
if(s!=0)
{
printf("pthread_cond_signal:");
return NULL;
}
return NULL;
}
int main(int argc,char *argv[])
{
int s,idx;
if(argc<2 || strcmp(argv[1],"--help") == 0)
{
printf("%s num-secs...\n",argv[0]);
return -1;
}
thread = calloc(argc-1,sizeof(*thread));
if(thread == NULL)
{
perror("calloc:");
return -1;
}
/* create all threads*/
for(idx=0;idx<argc-1;idx++){
thread[idx].sleepTime = atoi(argv[idx+1]);
thread[idx].state = TS_ALIVE;
s=pthread_create(&thread[idx].tid,NULL,threadFunc,&idx);
if(s!=0)
{
printf("pthread_create:");
return -1;
}
}
totThreads = argc-1;
numLive = totThreads;
/**join with terminated threads*/
while(numLive >0){
s = pthread_mutex_lock(&threadMutex);
if(s!=0)
{
printf("pthread_mutex_lock2:");
return -1;
}
while(numUnjoined == 0){
s = pthread_cond_wait(&threadDied,&threadMutex);
if(s!=0)
{
printf("pthread_cond_wait:");
return -1;
}
}
for(idx = 0;idx<totThreads;idx++){
if(thread[idx].state == TS_TERMINATED){
s = pthread_join(thread[idx].tid,NULL);
if(s!=0)
{
printf("pthread_cond_wait:");
return -1;
}
thread[idx].state = TS_JOINED;
numLive--;
numUnjoined--;
printf("Reaped thread %d(numLive=%d)\n",idx,numLive);
}
}
s=pthread_mutex_unlock(&threadMutex);
if(s!=0)
{
printf("pthread_mutex_unlock2:");
return -1;
}
}
exit(EXIT_SUCCESS);
}
30.2.5 经由动态分配的条件变量
使用函数pthread_cond_init()对条件变量进行动态初始化。需要使用pthread_cond_init()的情况类似于使用pthread_mutex_init()来动态初始化互斥量的情况。亦即,对自动或动态分配的条件变量进行初始化时,或是采用默认属性经由静态分配的条件进行初始化时,必须使用pthread_cond_init().
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond,
const pthread_condattr_t *attr);
Returns 0 on success, or a positive error number on error
参数cond表示将要初始化的目标条件变量。类似于互斥量,可以指定之前经由初始化处理的attr参数来判定条件变量的属性。对于attr所执向的pthread_condattr_t类型对象,可以使用多个Pthreads函数对其中属性进行初始化。若将attr置为NULL,则使用一组缺省属性来设置条件变量。
SUSv3规定,对业已初始化的条件变量进行再次初始化,将导致未定义的行为。应当避免这一做法。
当不在需要一个经由自动或动态分配的条件变量时,应调用pthread_cond_destroy()函数予以销毁,对于使用PTHREAD_COND_INITIALIZER进行静态初始化的条件变量,无需调用pthread_cond_destroy()。
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
Returns 0 on success,or a positive error number on error
对于某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是安全的。如果体哦阿健变量驻留于某片动态分配的内存区域,那么应该在内存释放之前将其销毁。经由自动分配的条件变量应在素质函数返回前予以销毁。
经由pthread_cond_destroy()销毁的变量,之后可以使用pthread_cond_init()对其进行重新初始化。
30.3总结
线程提供的强大共享是有代价的。多线程应用程序必须使用互斥量和条件变量等同步原语来协调对共享变量的访问。互斥量提供了对共享变量的独占式访问。条件变量允许一个或多个线程等候通知:其他线程改变了共享变量的状态。