TLPI读书笔记第30章:线程同步

本章介绍线程用来同步彼此行为的两个工具:互斥量( mutexe)和条件变量( condition variable)。互斥量可以帮助线程同步对共享资源的使用,以防如下情况发生:线程某甲试图访问一共享变量时,线程某乙正在对其进行修改。条件变量则是在此之外的拾遗补缺,允许线程相互通知共享变量(或其他共享资源)的状态发生了变化。

30.1 保护对共享变量的访问:互斥量

线程的主要优势在于,能够通过全局变量来共享信息。不过,这种便捷的共享是有代价的:必须确保多个线程不会同时修改同一变量,或者某一线程不会读取正由其他线程修改的变量。术语临界区(critical section) 是指访问某一共享资源的代码片段, 并且这段代码的执行应为原子(atomic)操作,亦即,同时访问同一共享资源的其他线程不应中断该片段的执行。

程序清单 30-1 中的简单示例,展示了以非原子方式访问共享资源时所发生的问题。该程序创建了两个线程,且均执行同一函数。该函数执行一个循环,重复以下步骤:将 glob 复制到本地变量 loc 中,然后递增 loc,再把 loc 复制回 glob,以此不断增加全局变量 glob 的值。

因为 loc 是分配于线程栈中的自动变量( automatic variable),所以每个线程都有一份。循环重复的次数要么由命令行参数指定,要么取默认值。

为避免线程更新共享变量时所出现问题,必须使用互斥量( mutex 是 mutual exclusion 的缩写)来确保同时仅有一个线程可以访问某项共享资源。更为全面的说法是,可以使用互斥量来保证对任意共享资源的原子访问,而保护共享变量是其最常见的用法。 互斥量有两种状态:已锁定( locked)和未锁定( unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁,将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。 一旦线程锁定互斥量,随即成为该互斥量的所有者。只有所有者才能给互斥量解锁。这一属性改善了使用互斥量的代码结构,也顾及到对互斥量实现的优化。

因为所有权的关系,有时会使用术语获取( acquire)和释放( release)来替代加锁和解锁。一般情况下,对每一共享资源(可能由多个相关变量组成)会使用不同的互斥量,每一线程在访问同一资源时将采用如下协议。

1.针对共享资源锁定互斥量

2.访问共享资源。

3.对互斥量解锁。 如果多个线程试图执行这一代码块(一个临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭到阻塞),即同时只有一个线程能够进入这段代码区域,如图 30-2 所示

最后请注意,使用互斥锁仅是一种建议,而非强制。亦即,线程可以考虑不使用互斥量而仅访问相应的共享变量。为了安全地处理共享变量,所有线程在使用互斥量时必须互相协调,遵守既定的锁定规则。

30.1.1 静态分配的互斥量

互斥量既可以像静态变量那样分配,也可以在运行时动态创建(例如,通过 malloc()在一块内存中分配)。动态互斥量的创建稍微有些复杂,将延后至 30.1.5 节再做讨论。 互斥量是属于 pthread_mutex_t 类型的变量。在使用之前必须对其初始化。对于静态分配的互斥量而言,可如下例所示,将 PTHREAD_MUTEX_INITIALIZER 赋给互斥量。

pthread_mutex_t  mtx=PTHREAD_MUTEX_INITIALIZER ;
30.1.2 加锁和解锁互斥量

初始化之后,互斥量处于未锁定状态。函数 pthread_mutex_lock()可以锁定某一互斥量,而函数 pthread_mutex_unlock()则可以将一个互斥量解锁。

#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t  *mutex);/*上锁*/
int pthread_mutex_unlock(pthread_mutex_t  *mutex);/*解锁*/
int pthread_mutex_trylock(pthread_mutex_t  *mutex);/*如果互斥量已经锁定,返回EBUSY错误*/
int pthread_mutex_timedlock(pthread_mutex_t  *mutex,const struct timespec abstime);/*规定时间内没有锁定互斥量,返回ETIMEDOUT错误*/

要锁定互斥量,在调用 pthread_mutex_lock()时需要指定互斥量。如果互斥量当前处于未锁定状态,该调用将锁定互斥量并立即返回。如果其他线程已经锁定了这一互斥量,那么 pthread_mutex_lock()调用会一直堵塞,直至该互斥量被解锁,到那时,调用将锁定互斥量并返回。 如果发起 pthread_mutex_lock()调用的线程自身之前已然将目标互斥量锁定,对于互斥量的默认类型而言,可能会产生两种后果—视具体实现而定:线程陷入死锁( deadlock),因试图锁定已为自己所持有的互斥量而遭到阻塞;或者调用失败,返回 EDEADLK 错误。

在 Linux上,默认情况下线程会发生死锁。 (30.1.7 节在讨论互斥量类型时会述及一些其他的可能行为。 ) 函数 pthread_mutex_unlock()将解锁之前已遭调用线程锁定的互斥量。以下行为均属错误: 对处于未锁定状态的互斥量进行解锁,或者解锁由其他线程锁定的互斥量。 如果有不止一个线程在等待获取由函数 pthread_mutex_unlock()解锁的互斥量,则无法判断究竟哪个线程将如愿以偿。

pthread_mutex_trylock()和 pthread_mutex_timedlock()

Pthreads API 提供了 pthread_mutex_lock()函数的两个变体: pthread_mutex_trylock()和 pthread_mutex_timedlock()。可参考手册页( manual page)获取这些函数的原型。 如果信号量已然锁定,对其执行函数 pthread_mutex_trylock()会失败并返回 EBUSY 错误,除此之外,该函数与 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。在 x86-32 架构的 Linux 2.6.31(含 NPTL)系统下运行这两个程序,如令单一线程循环 1000 万次,前者共花费了 0.35 秒(并产生错误结果),而后者则需要 3.1 秒。

乍看起来,代价极高。不过,考虑一下前者(程序清单 30-1)执行的主循环。在该版本中,函数 threadFunc()于 for 循环中,先递增循环控制变量,再将其与另一变量进行比较,随后执行两个复制操作和一个递增操作,最后返回循环起始处开始下一次循环。而后者—使用互斥 量的版本(程序清单 30-2)执行了相同步骤,不过在每次循环的前后多了加锁和解锁互斥量的工作。换言之,对互斥量加锁和解锁的开销略低于第 1 个程序的 10 次循环操作。 成本相对比较低廉。此外,在通常情况下,线程会花费更多时间去做其他工作,对互斥量的加锁和解锁操作相对要少得多,因此使用互斥量对于大部分应用程序的性能并无显著影响。

进而言之,在相同系统上运行一些简单的测试程序,结果显示,如将使用函数 fcntl()(见55.3 节)加锁、解锁一片文件区域的代码循环 2000 万次,需耗时 44 秒,而将对系统 V 信号量( semaphore)(见 47 章)的递增和递减代码循环 2000 万次,则需要 28 秒。

文件锁和信号量的问题在于,其锁定和解锁总是需要发起系统调用( system call),而每个系统调用的开销虽小,但颇为可观(见 3.1 节)。与之相反,互斥量的实现采用了机器语言级的原子操作(在内存中执行,对所有线程可见),只有发生锁的争用时才会执行系统调用。

Linux 上,互斥量的实现采用了 futex(源自“快速用户空间互斥量”[fast user space mutex]的首字母缩写),而对锁争用的处理则使用了 futex()系统调用。本书无意描述 futex,其设计意图也并非供用户空间( user space) 应用程序直接使用, 不过[Drepper, 2004(a)]给出了详细描述,还讨论了如何使用 futexes 来实现互斥量。 [Franke et al., 2002]是一篇由 futex 开发人员所撰写的论文(已经过时),介绍了 futex 的早期实现以及因其所带来的性能提升。

30.1.4 互斥量的死锁

有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就有可能发生死锁。图 30-3 展示了一个死锁的例子,其中每个线程都成功地锁住一个互斥量,接着试图对已为另一线程锁定的互斥量加锁。两个线程将无限期地等待下去。

要避免此类死锁问题,最简单的方法是定义互斥量的层级关系。当多个线程对一组互斥量操作时,总是应该以相同顺序对该组互斥量进行锁定。例如,在图 30-3 所示场景中,如果两个线程总是先锁定 mutex1 再锁定 mutex2,死锁就不会出现。有时,互斥量间的层级关系逻辑清晰。不过,即便没有,依然可以设计出所有线程都必须遵循的强制层级顺序。

另一种方案的使用频率较低,就是“尝试一下,然后恢复”。在这种方案中,线程先使用函数pthread_mutex_lock()锁定第1个互斥量,然后使用函数 pthread_mutex_trylock()来锁定其余互斥量。如果任pthread_mutex_trylock()调用失败(返回 EBUSY),那么该线程将释放所有互斥量,也许经过一段时间间隔,从头再试。较之于按锁的层级关系来规避死锁,这种方法效率要低一些,因为可能需要历经多次循环。另一方面,由于无需受制于严格的互斥量层级关系,该方法也更为灵活。 [Butenhof, 1996]中载有这一方案的范例。

30.1.5 动态初始化互斥量

静态初始值 PTHREAD_MUTEX_INITIALIZER,只能用于对如下互斥量进行初始化:经由静态分配且携带默认属性。其他情况下,必须调用 pthread_mutex_init()对互斥量进行动态初始化。

#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,const struct pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数 mutex 指定函数执行初始化操作的目标互斥量。参数 attr 是指向 pthread_mutexattr_t 类型对象的指针,该对象在函数调用之前已经过了初始化处理,用于定义互斥量的属性。(下节会介绍更多互斥量属性。 )若将 attr 参数置为 NULL,则该互斥量的各种属性会取默认值。

SUSv3 规定,初始化一个业已初始化的互斥量将导致未定义的行为,应当避免这一行为。在如下情况下,必须使用函数 pthread_mutex_init(),而非静态初始化互斥量。

1.动态分配于堆中的互斥量。例如,动态创建针对某一结构的链表,表中每个结构都包含一个 pthread_mutex_t 类型的字段来存放互斥量,借以保护对该结构的访问。

2.互斥量是在栈中分配的自动变量。

3.初始化经由静态分配,且不使用默认属性的互斥量。

当不再需要经由自动或动态分配的互斥量时,应使用 pthread_mutex_destroy()将其销毁。(对于使用 PTHREAD_MUTEX_INITIALIZER 静态初始化的互斥量,无需调用 pthread_mutex_destroy()。) 只有当互斥量处于未锁定状态,且后续也无任何线程企图锁定它时,将其销毁才是安全的。若互斥量驻留于动态分配的一片内存区域中,应在释放( 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 互斥量类型

前面几页对互斥量的行为做了若干论述。 1.同一线程不应对同一互斥量加锁两次。

2.线程不应对不为自己所拥有的互斥量解锁(亦即,尚未锁定互斥量)。

3.线程不应对一尚未锁定的互斥量做解锁动作。

准确地说,上述情况的结果将取决于互斥量类型。 SUSv3 定义了以下互斥量类型

PTHREAD_MUTEX_NORMAL

该类型的互斥量不具有死锁检测(自检)功能。如线程试图对已由自己锁定的互斥量加锁,则发生死锁。互斥量处于未锁定状态,或者已由其他线程锁定,对其解锁会导致不确定的结果。 (在 Linux 上,对这类互斥量的上述两种操作都会成功。 )

PTHREAD_MUTEX_ERRORCHECK

对此类互斥量的所有操作都会执行错误检查。所有上述 3 种情况都会导致相关 Pthreads 函数返回错误。这类互斥量运行起来比一般类型要慢,不过可将其作为调试工具,以发现程序在哪里违反了互斥量使用的基本原则。

PTHREAD_MUTEX_RECURSIVE

递归互斥量维护有一个锁计数器。当线程第 1 次取得互斥量时,会将锁计数器置 1。后续由同一线程执行的每次加锁操作会递增锁计数器的数值,而解锁操作则递减计数器计数。只有当锁计数器值降至 0 时,才会释放该互斥量。解锁时如目标互斥量处于未锁定状态,或是已由其他线程锁定,操作都会失败。

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类型互斥量的行为与 PTHREAD_MUTEX_NORMAL 类型相仿。程序清单 30-3 演示了如何设置互斥量类型,本例创建了一个带有错误检查属性(error-checking)的互斥量。

30.2 通知状态的改变:条件变量(Condition Variable)

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

/*生产者*/
static pthread_mutex_t mtx=PTHREAD_COND_INITALIZER ;
static avail=0;
s=pthread_mutex_lock(&mtx);
if(s!=0)
    errExit();
/*进行生产*/
avail++;
s=pthread_mutex_unlock(&mtx);
if(s!=0)
    errExit();
/*消费者*/
for(;;){
    s=pthread_mutex_lock(&mtx);
    if(s!=0)
        errExit();
    while(avail>0){
        /*进行消费*/
        avail--;
    }
    s=pthread_mutex_unlock(&mtx);
    if(s!=0)
        errExit();
}

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

30.2.1 由静态分配的条件变量

如同互斥量一样,条件变量的分配,有静态和动态之分。条件变量的动态创建延后到 30.2.5节再行描述,这里先讨论一下静态分配。 条件变量的数据类型是 pthread_coud_t。类似于互斥量,使用条件变量前必须对其初始化。对于经由静态分配的条件变量,将其赋值为 PTHREAD_COND_INITALIZER 即完成初始化操作。可参考下面的例子:

pthread_coud_t cond=PTHREAD_COND_INITIALIZER
30.2.2 通知和等待条件变量

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

#include<pthread.h>
int pthread_cond_signal(pthread_coud_t *cond);   /*只唤醒一个*/
int pthread_cond_broadcast(pthread_coud_t *cond);/*唤醒所有*/
int pthread_cond_wait(pthread_coud_t *cond,
                      pthread_mutex_t *mutex);     /*等待*/
int pthread_cond_timewait(pthread_coud_t *cond,
                          pthread_mutex_t *mutex,
                          const struct timespec abstime);     /*休眠一段时间*/

函数 pthread_cond_signal()和 pthread_cond_broadcast()均可针对由参数 cond 所指定的条件变量而发送信号。 pthread_cond_wait()函数将阻塞一线程,直至收到条件变量 cond 的通知。 函数 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 来指定一个线程等待条件变量通知时休眠时间的上限

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

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

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

/*修改后的生产者代码*/
static pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
static avail=0;
s=pthread_mutex_lock(&mtx);
if(s!=0)
    errExit("pthread_mutex_lock")
/*进行生产*/
avail++;
s=pthread_mutex_unlock(&mtx);
if(s!=0)
    errExit("pthread_mutex_unlock")
s=pthread_cond_signal(&cond);/*新增的代码*/
if(s!=0)
    errExit("pthread_cond_signal");

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

1.解锁互斥量 mutex。

2.堵塞调用线程,直至另一线程就条件变量 cond 发出信号。

3.重新锁定 mutex。

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

s=pthread_mutex_lock(&mtx);
if(s!=0)
    errExit("pthread_mutex_lock")
while(/*不符合条件*/)
    pthread_cond_wait(&cond,&mtx)
s=pthread_mutex_unlock(&mtx);
if(s!=0)
    errExit("pthread_mutex_unlock")

下一节将会介绍为何将 pthread_cond_wait()调用置于 while 循环中,而非 if 语句中。 在以上代码中,两处对共享变量的访问都必须置于互斥量的保护之下,其原因之前已做了解释。换言之,条件变量与互斥量之间存在着天然的关联关系。

1.线程在准备检查共享变量状态时锁定互斥量。

2.检查共享变量的状态。

3.如果共享变量未处于预期状态,线程应在等待条件变量并进入休眠前解锁互斥量(以便其他线程能访问该共享变量)。

4.当线程因为条件变量的通知而被再度唤醒时, 必须对互斥量再次加锁, 因为在典型情况下,线程会立即访问共享变量。 函数 pthread_cond_wait()会自动执行最后两步中对互斥量的解锁和加锁动作。第 3 步中互斥 量 的 释 放 与 陷 入 对 条 件 变 量 的 等 待 同 属 于 一 个 原 子 操 作 。 换 句 话 说 , 在 函 数pthread_cond_wait()的调用线程陷入对条件变量的等待之前, 其他线程不可能获取到该互斥量,也不可能就该条件变量发出信号。

/*消费者代码*/
for(;;){
    s=pthread_mutex_lock(&mtx);
    if(s!=0)
        errExit();
    while(avail==0){/*等待消费*/
        s=pthread_cond_wait(&cond,&mtx);
        if(s!=0)
        errExit();
    }
    
    while(avail>0){/*进行消费*/
        avail--;
    }
    s=pthread_mutex_unlock(&mtx);
    if(s!=0)
        errExit();
}

最后,再看一下 pthread_cond_signal()和 pthread_cond_broadcast()的使用。前面展示的生产者代码先调用了 pthread_mutex_unlock(),接着调用了 pthread_cond_signal();换言之,先解锁与共享变量相关的互斥量, 再就对应的条件变量发出信号。 也可以将这两步颠倒执行, SUSv3允许以任意顺序执行这两个调用

30.2.3 测试条件变量的判断条件( predicate)

每个条件变量都有与之相关的判断条件,涉及一个或多个共享变量。例如,在上一节的代码中, 与 cond 相关的判断是(avail == 0)。 这段代码展示了一个通用的设计原则: 必须由一个 while循环,而不是 if 语句,来控制对 pthread_cond_wait()的调用。这是因为,当代码从 pthread_cond_wait()返回时,并不能确定判断条件的状态,所以应该立即重新检查判断条件,在条件不满足的情况下继续休眠等待。

从 pthread_cond_wait()返回时,之所以不能对判断条件的状态做任何假设,其理由如下。

1.其他线程可能会率先醒来。也许有多个线程在等待获取与条件变量相关的互斥量。即使就互斥量发出通知的线程将判断条件置为预期状态, 其他线程依然有可能率先获取互斥量并改变相关共享变量的状态,进而改变判断条件的状态。

2.设计时设置“宽松的”判断条件或许更为简单。有时,用条件变量来表征可能性而非确定性,在设计应用程序时会更为简单。换言之,就条件变量发送信号意味着“可能有些事情”需要接收信号的线程去响应,而不是“一定有一些事情”要做。使用这种方法,可以基于判断条件的近似情况来发送条件变量通知,接收信号的线程可以通过再次检查判断条件来确定是否真的需要做些什么。

3.可能会发生虚假唤醒的情况。在一些实现中,即使没有任何其他线程真地就条件变量发出信号,等待此条件变量的线程仍有可能醒来。在一些多处理器系统上,为确保高效实现而采用的技术会导致此类(不常见的)虚假唤醒。 SUSv3 对此予以明确认可

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);
int pthread_cond_destroy(pthread_cond_t *cond);

参数 cond 表示将要初始化的目标条件变量。类似于互斥量,可以指定之前经由初始化处理的 attr 参数来判定条件变量的属性。对于 attr 所指向的 pthread_condattr_t 类型对象,可使用多个 Pthreads 函数对其中属性进行初始化。若将 attr 置为 NULL,则使用一组缺省属性来设置条件变量。 SUSv3 规定,对业已初始化的条件变量进行再次初始化,将导致未定义的行为。应当避免这一做法。

当不再需要一个经由自动或动态分配的条件变量时,应调用 pthread_cond_destroy()函数予以销毁。对于使用 PTHREAD_COND_INITIALIZER 进行静态初始化的条件变量,无需调用pthread_cond_destroy()。 对某个条件变量而言,仅当没有任何线程在等待它时,将其销毁才是安全的。如果条件变量驻留于某片动态创建的内存区域,那么应在释放该内存区域前就将其销毁。经由自动分配的条件变量应在宿主函数返回前予以销毁。 经 pthread_cond_destroy()销毁的条件变量,之后可以调用 pthread_cond_init()对其进行重新初始化。

30.3 总结

线程提供的强大共享是有代价的。多线程应用程序必须使用互斥量和条件变量等同步原语来协调对共享变量的访问。互斥量提供了对共享变量的独占式访问。条件变量允许一个或多个线程等候通知:其他线程改变了共享变量的状态

原文地址:https://www.cnblogs.com/wangbin2188/p/14808618.html