同步和互斥在多线程和多进程编程中是一个基本的需求,互相协作的多个进程和线程往往需要某种方式的同步和互斥。POSIX定义了一系列同步对象用于同步和互斥。
同步对象是内存中的变量属于进程中的资源,可以按照与访问数据完全相同的方式对其进行访问。默认情况下POSIX定义的这些同步对象具有进程可见性,即同步对象只对定义它的进程可见;但是通过修改同步对象的属性可以使得同步对象对不同的进程可见,具体的做法是:
- 修改同步对象的属性为PTHREAD_PROCESS_SHARED
- 在进程的特殊内存区域--共享内存中创建同步对象
这样创建的同步对象将对共享该共享内存的所有进程可见,这些进程可以使用该同步对象进行同步互斥。
其中设置共享对象的属性为PTHREAD_PROCESS_SHARED是为了告诉系统该共享对象是跨越进程的,不仅仅对创建它的进程可见;但是仅有这一个条件显然无法满足不同进程使用该同步对象的需求,因为每个进程的地址空间是独立的,位于一个进程的普通内存区域中的对象是无法被其它进程所访问的,能满足这一要求的内存区域是共享内存,因而同步对象要在进程的共享内存区域内创建。
同步对象还可以放在文件中。同步对象可以比创建它的进程具有更长的生命周期。
POSIX定义的同步对象包括:
- 互斥锁
- 条件变量
- 自旋锁
- 读写锁
- 信号量
对于这些同步对象,有一些共同点:
- 每种类型的同步对象都有一个init的API,它完成该对象的初始化,在初始化过程中会分配该同步对象所需要的资源(注意是为支持这种锁而需要的资源,不包括表示同步对象的变量本身所需要的内存)
- 每种类型的同步对象都一个destory的API,它完成与init相反的工作
- 对于使用动态分配内存的同步对象,在使用它之前必须先调用init
- 在释放使用动态分配内存的同步对象所使用的内存时,必须先调用destory释放系统为其申请的资源
- 每种同步对象的默认作用范围都是进程内部的线程,但是可以通过修改其属性为PTHREAD_PROCESS_SHARED并在进程共享内存中创建它的方式使其作用范围跨越进程范围
- 无论是作用于进程内的线程,还是作用于不同进程间的线程,真正参与竞争的都是线程(对于不存在多个线程的进程来说就是其主线程),因而讨论都基于线程来
- 这些同步对象都是协作性质的,相当于一种君子协定,需要相关线程主动去使用,无法强制一个线程必须使用某个同步对象
总体上来说,可以将它们分为两类:
- 第一类是互斥锁、读写锁、自旋锁,它们主要是用来保护临界区的,也就是主要用于解决互斥问题的,当尝试上锁时大体上有两种情况下会返回:上锁成功或出错,它们不会因为出现信号而返回。另外解锁只能由锁的拥有着进行
- 第二类是条件变量和信号量,它们提供了异步通知的能力,因而可以用于同步和互斥。但是二者又有区别:
- 信号量可以由发起P操作的线程发起V操作,也可以由其它线程发起V操作;但是条件变量一般要由其它线程发起signal(即唤醒)操作
- 由于条件变量并没有包含任何需要检测的条件的信息,因而对这个条件需要用其它方式来保护,所以条件变量需要和互斥锁一起使用,而信号量本身就包含了相关的条件信息(一般是资源可用量),因而不需要和其它方式一起来使用
- 类似于三种锁,信号量的P操作要么成功返回,要么失败返回,不会因而出现信号而返回;但是条件变量可能因为出现信号而返回,这也是因为它没包含相关的条件信息而导致的。
一、互斥锁
1.基本概念
互斥锁(mutex):顾名思义即是相互排斥的锁,它是最基本的同步形式,用于保护临界区,以保证任何时刻都只有一个线程在执行临界区中的代码。在任意时刻都只有一个线程能持有这把锁,当已经有线程拿到这把锁的时候,其它请求获得这把锁的线程将会被阻塞直到持有锁的线程释放了这把锁或者得到一个EBUSY的错误。
临界区的基本形式为:
mutex_lock(...);
临界区
mutex_unlock(...);
即位于lock之后,unlock之前的部分是临界区。
互斥锁用来解决互斥问题非常合适,因为它可以保证同一时刻只有一个线程能拿到这把锁,进而执行其临界区中的代码。最常见互斥场景是:多个线程会访问它们共享的资源,为了保证资源的一致性,因而需要进行互斥访问,以保证任何时刻都只有一个线程在访问该资源。因而大部分场景下临界区中放的都是操作互斥资源的代码。
互斥锁解决同步问题就不是那么合适了,因为需要同步的线程之间往往有依赖或者顺序关系,但是互斥锁自己无法保证这个顺序。
如果有多个线程在等待一个互斥锁,则在持有互斥锁的线程释放锁后锁将被等待锁的线程中具有最高优先级的那个获得,如果最高优先级线程有多个,则这些线程中谁将获得锁是不确定的。
2.API
POSIX定义的互斥锁的数据类型是: pthread_mutex_t
相关API
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mp, const pthread_mutexattr_t *mattr); 成功返回0,其它返回值表示出错
int pthread_mutex_consistent_np(pthread_mutex_t *mutex); 成功返回0,其它返回值表示出错
int pthread_mutex_lock(pthread_mutex_t *mutex); 成功返回0,其它返回值表示出错
int pthread_mutex_unlock(pthread_mutex_t *mutex); 成功返回0,其它返回值表示出错
int pthread_mutex_trylock(pthread_mutex_t *mutex); 成功返回0,其它返回值表示出错
int pthread_mutex_destroy(pthread_mutex_t *mp); 成功返回0,其它返回值表示出错
int pthread_mutexattr_init(pthread_mutexattr_t *mattr);成功返回0,其它返回值表示出错
int pthread_mutexattr_destroy(pthread_mutexattr_t *mattr);成功返回0,其它返回值表示出错
int pthread_mutexattr_setpshared(pthread_mutexattr_t *mattr,int pshared);成功返回0,其它返回值表示出错
int pthread_mutexattr_getpshared(pthread_mutexattr_t *mattr,int *pshared);成功返回0,其它返回值表示出错
int pthread_mutexattr_settype(pthread_mutexattr_t *attr , int type);成功返回0,其它返回值表示出错
int pthread_mutexattr_gettype(pthread_mutexattr_t *attr , int *type);成功返回0,其它返回值表示出错
int pthread_mutexattr_setprotocol(pthread_mutexattr_t *attr, int protocol);成功返回0,其它返回值表示出错
int pthread_mutexattr_getprotocol(const pthread_mutexattr_t *attr, int *protocol);成功返回0,其它返回值表示出错
int pthread_mutexattr_setprioceiling(pthread_mutexatt_t *attr, int prioceiling, int *oldceiling);成功返回0,其它返回值表示出错
int pthread_mutexattr_getprioceiling(const pthread_mutexatt_t *attr, int *prioceiling);成功返回0,其它返回值表示出错
int pthread_mutex_setprioceiling(pthread_mutex_t *mutex, int prioceiling, int *old_ceiling);成功返回0,其它返回值表示出错
int pthread_mutex_getprioceiling(const pthread_mutex_t *mutex, int *prioceiling);成功返回0,其它返回值表示出错
int pthread_mutexattr_setrobust_np(pthread_mutexattr_t *attr, int *robustness);成功返回0,其它返回值表示出错
int pthread_mutexattr_getrobust_np(const pthread_mutexattr_t *attr, int *robustness);成功返回0,其它返回值表示出错
1)初始化互斥锁
如果互斥锁变量是静态的则可以直接用PTHREAD_MUTEX_INITIALIZER来初始化它,比如:
static pthread_mutex_t my_lock = PTHREAD_MUTEX_INITIALIZER
如果互斥锁变量是动态分配的,则必须在使用它之前用pthread_mutex_init来初始化它.
pthread_mutex_init用于初始化互斥锁,如果mattr为NULL则用缺省值初始化由mp所指向的互斥锁,否则使用指定的mattr初始化互斥锁。
使用PTHREAD_MUTEX_INITIALIZER与动态分配具有null 属性的 pthread_mutex_init等效,不同之处在于PTHREAD_MUTEX_INITIALIZER 宏不进行错误检查。
如果使用pthread_mutex_init初始化互斥锁,并且指定的mattr具有PTHREAD_MUTEX_ROBUST_NP属性,则互斥锁所使用的内存必须在调用pthread_mutex_init之前被清0.
在有线程正在使用互斥锁时,不能重新初始化互斥锁或销毁它。
2)使互斥保持一致
如果一个互斥锁的持有者没有释放该锁退出了,则在默认情况下当其它线程再去获取这个锁的时候,就会阻塞从而造成死锁。可以更改互斥锁的属性来改变这种默认的方式:
pthread_mutexattr_setprotocol(&mattr, PTHREAD_PRIO_INHERIT);
pthread_mutexattr_setrobust_np(&mattr,PTHREAD_MUTEX_ROBUST_NP);
通过设置锁的上面两个属性,互斥锁就不再具有默认的行为,当一个锁的owner死掉后,其它线程再去获取这个锁的时候,不会被阻塞,而是会获得这个错,但是同时会得到一个EOWNERDEAD的错误。
然后获得锁的线程可以尝试处理这个错误:
- 首先调用pthread_mutex_consistent_np函数来恢复该锁的一致性,
- 然后调用pthread_mutex_unlock来解锁
- 接下来在调用加锁
这样该锁的行为就恢复正常了。
如果pthread_mutex_consistent_np在恢复锁的一致性时候没有成功,步骤c就不能再执行了,锁也不能被使用了,而且接下来的线程在获取锁都无法获得该锁,而是只能得到返回值ENOTRECOVERABLE。
如果获取某个锁的时候得到了ENOTRECOVERABLE的错误,就意味这这个锁不能被使用了,此时只能调用pthread_mutex_destroy销毁互斥锁然后再调用pthread_mutex_int重新初始化该互斥锁,之后才能再使用该互斥锁。
3)锁定(获取)互斥锁
pthread_mutex_lock可以锁定指定的互斥锁。
当它返回时,该互斥锁已被锁定,调用它的线程就获得了这个互斥锁。如果该互斥锁已被另一个线程锁定和拥有,则调用线程将阻塞,直到该互斥锁变为可用为止。
互斥锁的类型不同,pthread_mutex_lock的行为也有所不同。
4)解除互斥锁锁定(释放互斥锁)
pthread_mutex_unlock可以解除指定互斥锁的锁定即释放互斥锁。
5)尝试锁定(获取)互斥锁
pthread_mutex_trylock可以尝试锁定指定的互斥锁。
pthread_mutex_trylock是 pthread_mutex_lock的非阻塞版本。如果 mutex 所引用的互斥对象当前被任何线程锁定,则将立即返回该调用。否则,该互斥锁将被锁定,调用线程成为其持有者。
6)销毁互斥锁
pthread_mutex_destroy可以销毁与指定的互斥锁相关联的任何状态。
7)初始化互斥锁属性对象
互斥锁具有一些属性,通过修改这些属性可以控制锁的一些行为。缺省的互斥锁属性及其值如下:
- pshared: PTHREAD_PROCESS_PRIVATE
- type: PTHREAD_MUTEX_DEFAULT
- protocol: PTHREAD_PRIO_NONE
- prioceiling: –
- robustness: PTHREAD_MUTEX_STALLED_NP
可以用pthread_mutexattr_init将与互斥锁对象相关联的属性初始化为其缺省值。pthread_mutexattr_init的参数类型实际上是opaque的,其中包含一个由系统分配的属性对象。该函数执行过程中会为属性对象分配所需的内存,因而如果未通过pthread_mutexattr_destroy销毁互斥锁属性对象时就会导致内存泄漏。
对于互斥锁属性对象,必须首先通过调用pthread_mutexattr_destroy将其销毁,才能重新初始化该对象。
8)销毁互斥锁属性对象
pthread_mutexattr_destroy销毁指定的互斥锁属性对象,实际上它完成了释放由pthread_mutexattr_init分配的内存的过程。
9)设置/获取互斥锁的作用域属性
函数pthread_mutexattr_setpshared用来设置互斥锁的作用域。
互斥锁变量可以是进程专用的变量,也可以是跨越进程边界的变量。
范围属性的取值及其含义:
- PTHREAD_PROCESS_SHARED:具有该属性的互斥锁可以在多个进程中的线程之间共享。
- PTHREAD_PROCESS_PRIVATE:只有创建本互斥锁的线程所在的进程内的线程才能够使用该互斥锁变量。该值是缺省值。
函数pthread_mutexattr_getpshared可用来返回由 pthread_mutexattr_setpshared设置的互斥锁变量的范围。
10)设置/获取互斥锁的类型属性
pthread_mutexattr_settype用来设置指定互斥锁的类型属性。类型属性的缺省值为 PTHREAD_MUTEX_DEFAULT。
互斥锁的类型及其行为:
- PTHREAD_MUTEX_NORMAL:不提供死锁检测。尝试重新锁定互斥锁会导致死锁。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或未锁定,则将产生不确定的行为。
- PTHREAD_MUTEX_ERRORCHECK:提供错误检查。如果某个线程尝试重新锁定的互斥锁已经由该线程锁定,则将返回错误。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误。
- PTHREAD_MUTEX_RECURSIVE:该互斥锁会保留锁定计数这一概念。线程首次成功获取互斥锁时,锁定计数会设置为 1。线程每重新锁定该互斥锁一次,锁定计数就增加 1。线程每解除锁定该互斥锁一次,锁定计数就减小 1。 锁定计数达到 0 时,该互斥锁即可供其他线程获取。如果某个线程尝试解除锁定的互斥锁不是由该线程锁定或者未锁定,则将返回错误。
- PTHREAD_MUTEX_DEFAULT:尝试以递归方式锁定该互斥锁将产生不确定的行为。对于不是由调用线程锁定的互斥锁,如果尝试解除对它的锁定,则会产生不确定的行为。如果尝试解除锁定尚未锁定的互斥锁,则会产生不确定的行为。
在linux中互斥锁的相关类型定义如下(最好的办法是检查pthread.h这个头文件):
#if defined __USE_UNIX98 || defined __USE_XOPEN2K8
,
PTHREAD_MUTEX_NORMAL = PTHREAD_MUTEX_TIMED_NP,
PTHREAD_MUTEX_RECURSIVE = PTHREAD_MUTEX_RECURSIVE_NP,
PTHREAD_MUTEX_ERRORCHECK = PTHREAD_MUTEX_ERRORCHECK_NP,
PTHREAD_MUTEX_DEFAULT = PTHREAD_MUTEX_NORMAL
#endif
pthread_mutexattr_gettype用来获取由pthread_mutexattr_settype设置的互斥锁的类型属性。
11)设置/获取互斥锁的协议属性
pthread_mutexattr_setprotocol用来设置互斥锁的协议属性。
互斥锁协议属性的可能值及其含义:
- PTHREAD_PRIO_NONE: 线程的优先级和调度不会受到互斥锁拥有权的影响.
- PTHREAD_PRIO_INHERIT: 此协议值会影响拥有该互斥锁的线程的优先级和调度。如果更高优先级的线程因thrd1所拥有的一个或多个互斥锁而被阻塞,而这些互斥锁是用 PTHREAD_PRIO_INHERIT 初始化的,则thrd1的运行优先级为优先级pri1和优先级pri2中优先级较高的那一个,其中
- thrd1的优先级为pri1
- 所有正在等待这些互斥锁(这些互斥锁是 thrd1指所拥有的互斥锁)的线程的最高优先级为pri2
如果thrd1因另一个线程(thrd3) 拥有的互斥锁而被阻塞,则相同的优先级继承效应会以递归方式传播给thrd3。
使用PTHREAD_PRIO_INHERIT可以避免优先级逆转。当低优先级的线程持有较高优先级线程所需的锁时,就会发生优先级逆转。此时只有在较低优先级的线程释放该锁之后,较高优先级的线程才能继续执行。
如果没有优先级继承,底优先级的线程可能会在很长一段时间内都得不到调度,而这会导致等待低优先级线程锁持有的锁的高优先级线程也等待很长时间(因为低优先级线程无法运行,因而就无法释放锁,所以高优先级线程只能继续阻塞在锁上)。使用优先级继承可以短时间的提高低优先级线程的优先级,从而使它可以尽快得到调度,然后释放锁。低优先级线程在释放锁后就会恢复自己的优先级。
- PTHREAD_PRIO_PROTECT:当线程拥有一个或多个使用PTHREAD_PRIO_PROTECT 初始化的互斥锁时,线程的优先级和调度会受到影响。线程将以优先级pri1和优先级pri2中优先级较高的那一个优先级来运行,其中
- 线程的优先级为pri1
- 所有被线程持有的锁的最高优先级为pri2
被该线程所持有的锁阻塞的高优先级线程对该线程的调度没有影响。
PTHREAD_PRIO_INHERIT 和 PTHREAD_PRIO_PROTECT 只有在采用实时调度策略SCHED_FIFO 或 SCHED_RR的优先级进程内可用。(The PTHREAD_PRIO_INHERIT and PTHREAD_PRIO_PROTECT mutex attributes are usable only by privileged processes running in the realtime (RT) scheduling class SCHED_FIFO or SCHED_RR.)
一个线程可以同时拥有多个混合使用 PTHREAD_PRIO_INHERIT 和 PTHREAD_PRIO_PROTECT协议属性初始化的互斥锁。在这种情况下,该线程将以通过其中任一协议获取的最高优先级执行。
pthread_mutexattr_getprotocol可用来获取互斥锁属性对象的协议属性。
12)设置/获取互斥锁属性对象的优先级上限属性
pthread_mutexattr_setprioceiling用来设置指定互斥锁属性对象的优先级上限属性。
prioceiling指定已初始化互斥锁的优先级上限。优先级上限定义执行互斥锁保护的临界段时的最低优先级。prioceiling 位于 SCHED_FIFO 所定义的优先级的最大范围内。要避免优先级倒置,请将 prioceiling 设置为高于或等于可能会锁定特定互斥锁的所有线程的最高优先级。
oldceiling 用于返回以前的优先级上限值。
pthread_mutexattr_getprioceiling用来获取互斥锁属性对象的优先级上限属性。
13)设置/获取互斥锁对象的优先级上限属性
pthread_mutex_setprioceiling可更改互斥锁 mutex 的优先级上限 prioceiling。
pthread_mutex_setprioceiling可锁定互斥锁(如果未锁定的话),或者一直处于阻塞状态,直到它成功锁定该互斥锁,更改该互斥锁的优先级上限并将该互斥锁释放为止。锁定互斥锁的过程无需遵循优先级保护协议。
如果 pthread_mutex_setprioceiling成功,则将在 old_ceiling 中返回以前的优先级上限值。如果pthread_mutex_setprioceiling失败,则互斥锁的优先级上限保持不变。
pthread_mutex_getprioceiling会返回 mutex 的优先级上限 prioceiling。
14)设置/获取互斥锁的强健属性
pthread_mutexattr_setrobust_np用来设置互斥锁属性对象的强健属性。仅当定义了符号 _POSIX_THREAD_PRIO_INHERIT 时,pthread_mutexattr_setrobust_np()才适用。
robustness 定义在互斥锁的持有者“死亡”时的行为。pthread.h 中定义的 robustness 的值为PTHREAD_MUTEX_ROBUST_NP 或 PTHREAD_MUTEX_STALLED_NP。缺省值为PTHREAD_MUTEX_STALLED_NP。
- PTHREAD_MUTEX_STALLED_NP: 如果互斥锁的持有者死亡,则以后对 pthread_mutex_lock() 的所有调用将以不确定的方式被阻塞。
- PTHREAD_MUTEX_ROBUST_NP: 如果互斥锁的持有者“死亡”了,或者持有这样的互斥锁的进程unmap了互斥锁所在的共享内存或者持有这样的互斥锁的进程执行了exec调用,则会解除锁定该互斥锁。互斥锁的下一个持有者将获取该互斥锁,并返回错误 EOWNWERDEAD。
如果互斥锁具有PTHREAD_MUTEX_ROBUST_NP的属性,则应用程序在获取该锁时必须检查 pthread_mutex_lock 的返回代码看获取锁时是否返回了EOWNWERDEAD错误。如果是,则
- 互斥锁的新的持有者应使该互斥锁所保护的状态保持一致。因为互斥锁的上一个持有者“死亡”时互斥锁所保护的状态可能出于不一致的状态。
- 如果互斥锁的新的持有者能够使该状态保持一致,请针对该互斥锁调用pthread_mutex_consistent_np(),并解除锁定该互斥锁。
- 如果互斥锁的新的持有者无法使该状态保持一致,请勿针对该互斥锁调用pthread_mutex_consistent_np(),而是解除锁定该互斥锁。所有等待的线程都将被唤醒,以后对 pthread_mutex_lock() 的所有调用都将无法获取该互斥锁。返回错误为ENOTRECOVERABLE。
如果一个线程获取了互斥锁,但是获取时得到了EOWNERDEAD的错误,然后它终止并且没有释放互斥锁 ,则下一个持有者获取该锁时将返回代码EOWNERDEAD。
二、条件变量
1.基本概念
不同于互斥锁,互斥锁主要用于上锁,而条件变量用于等待。与条件变量相关的条件由程序自己定义并由程序自己检查。由于条件变量是用于等待的,因而它特别适合需要进行同步的问题,比如线程A,B存在依赖关系,B要在某个条件发生之后才能继续执行,而这个条件只有A才能满足,这个时候就可以使用条件变量来完成这个事情:
- 创建和该条件相关联的条件变量,并初始化它
- 对于线程A来说,它需要做的是设置这个条件,通知等待在相关联条件变量上的线程
- 对于线程B来说,它需要做的是检查这个条件,如果不满足自己的要求,就阻塞在相关联的条件变量上
由于条件变量并没有包含任何需要检测的条件的信息,因而对这个条件需要用其它方式来保护,所以条件变量需要和互斥锁一起使用,每个条件变量总是有一个互斥锁和其关联。如果线程未持有与条件相关联的互斥锁,则调用pthread_cond_signal或pthread_cond_broadcast会产生唤醒丢失错误,满足以下所有条件时,即会出现唤醒丢失问题:
- 一个线程调用 pthread_cond_signal或 pthread_cond_broadcast
- 另一个线程已经测试了该条件,但是尚未调用 pthread_cond_wait
- 没有正在等待的线程,因而pthread_cond_signal或 pthread_cond_broadcast的唤醒将无法起作用,该唤醒会被丢失
对比下信号,信号可以做到通知其它线程某件事发生了,接收信号的线程只需要注册一个信号处理函数,然后信号发生后该处理函数就会被系统调用,一旦该函数被调用了就意味着注册时关联的信号所代表的事情发生了。但要注意:
- POSXI要求多线程应用中信号处理程序必须在应用的多个线程之间共享(即在一个进程的多个线程之间共享),因而对于同一个进程中的多个线程来说它们必须共享信号处理程序,信号处理程序无法确定信号是被发给谁的
- 使用信号时只需要注册信号处理程序即可,不需要创建某种同步对象,而使用条件变量需要创建同步对象,如果要在进程间进行同步和互斥还对条件变量的作用域和属性有要求
- 有权限的任何用户的任何程序都可以发送信号给一个线程,而使用条件变量时,相关的线程必须可以访问同步对象
2.API
POSIX定义的条件变量的数据类型是: pthread_cond_t
相关API
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cv, const pthread_condattr_t *cattr);成功返回0,其它返回值表示出错
int pthread_cond_wait(pthread_cond_t *cv,pthread_mutex_t *mutex);成功返回0,其它返回值表示出错
int pthread_cond_signal(pthread_cond_t *cv);成功返回0,其它返回值表示出错
int pthread_cond_timedwait(pthread_cond_t *cv, pthread_mutex_t *mp, const struct timespec *abstime);成功返回0,其它返回值表示出错
int pthread_cond_reltimedwait_np(pthread_cond_t *cv, pthread_mutex_t *mp, const struct timespec *reltime);成功返回0,其它返回值表示出错
int pthread_cond_broadcast(pthread_cond_t *cv);成功返回0,其它返回值表示出错
int pthread_cond_destroy(pthread_cond_t *cv);成功返回0,其它返回值表示出错
1)初始化条件变量
如果条件变量变量是静态的则可以直接用PTHREAD_COND_INITIALIZER来初始化它,比如:
static pthread_cond_t my_cond = PTHREAD_COND_INITIALIZER
如果条件变量是动态分配的,则必须在使用它之前用pthread_cond_init来初始化它。
pthread_cond_init用来初始化cv所指向的条件变量,如果cattr为NULL则会用缺省的属性初始化条件变量;否则使用cattr指定的属性初始化条件变量。
使用PTHREAD_COND_INITIALIZER 宏与动态分配具有null 属性的 pthread_cond_init()等效,不同之处在于PTHREAD_COND_INITIALIZER 宏不进行错误检查。
多个线程决不能同时初始化或重新初始化同一个条件变量。如果要重新初始化或销毁某个条件变量,则应用程序必须确保该条件变量未被使用。
2)基于条件变量阻塞
pthread_cond_wait以原子方式释放mutex所指向的互斥锁,并导致调用线程阻塞在cv所指向的条件变量上。
阻塞的线程可以通过如下方式被唤醒:
- 由pthread_cond_signal唤醒
- 由pthread_cond_broadcast唤醒
- 由信号唤醒
pthread_cond_wait返回时,由mutex指定的互斥锁被锁定并且被调用线程锁持有,即使返回错误时也是如此。
pthread_cond_wait在被唤醒之前将一致保持阻塞状态。它会在被阻塞之前以原子方式释放相关的互斥锁,并在返回之前以原子方式再次获取该互斥锁。
通常情况下对条件表达式的检查是在互斥锁的保护下进行的。如果条件表达式为假,线程就会基于条件变量阻塞。然后,当其它线程更改条件值时,就会唤醒它(通过pthread_cond_signal或pthread_cond_broadcast)。这种变化会导致至少一个正在等待该条件的线程解除阻塞并尝试再次获取互斥锁。
必须重新测试导致等待的条件,然后才能从 pthread_cond_wait处继续执行。唤醒的线程重新获取互斥锁并从pthread_cond_wait返回之前,条件可能会发生变化。等待线程锁等待的条件可能并未真正发生。通常使用条件变量的方式如下:
pthread_mutex_lock();
while(condition_is_false)
pthread_cond_wait();
pthread_mutex_unlock();
pthread_cond_wait是一个取消点。如果有一个未决的取消请求并且该线程启用了取消功能,则该线程会被终止并在继续持有锁的状态下开始执行的清理处理函数。如果清理处理函数中未释放锁,则就会出现线程终止但是未释放锁的情形。
3)解除阻塞线程
pthread_cond_signal解除阻塞在该条件变量上的一个线程的阻塞状态。
应在互斥锁的保护下修改相关条件,该互斥锁应该是与该条件变量相关联的那个互斥锁(即调用wati时指定的那个互斥锁)。否则,可能在条件变量的测试和pthread_cond_wait阻塞之间修改该变量,这会导致无限期等待。
如果有多个线程在等待一个条件变量,则线程被唤醒的顺序由所采用的调度策略决定。
- 如果使用的是默认的调度策略,即SCHED_OTHER,则无法保证被唤醒的顺序
- 如果使用的是SCHED_FIFO 或SCHED_RR,则线程按照优先级被唤醒
如果没有任何线程基于条件变量阻塞,则调用 pthread_cond_signal不起作用。
4)在指定的时间之前阻塞
pthread_cond_timedwait的用法与 pthread_cond_wait的用法基本相同,区别在于在由abstime指定的时间之后不再被阻塞。
pthread_cond_reltimedwait_np与pthread_cond_timedwait基本相同,它们唯一的区别在于pthread_cond_reltimedwait_np使用相对时间间隔而不是将来的绝对时间作为其最后一个参数的值。
类似于pthread_cond_wait,pthread_cond_reltimedwait_np和pthread_cond_timedwait也是取消点。
5)解除阻塞所有线程
pthread_cond_broadcast解除所有基于该条件变量阻塞的线程的阻塞。
应在互斥锁的保护下修改相关条件,该互斥锁应该是与该条件变量相关联的那个互斥锁(即调用wati时指定的那个互斥锁)。否则,可能在条件变量的测试和pthread_cond_wait阻塞之间修改该变量,这会导致无限期等待。
由于pthread_cond_broadcast会导致所有基于该条件阻塞的线程再次争用互斥锁,因此即便使用了pthread_cond_broadcast实际上最终也只有一个线程可以获得锁并开始运行。虽然都是只有一个线程可以运行,但是这种情形与pthread_cond_signal是有所区别的:
- 如果有多个线程阻塞在条件变量上,并且pthread_cond_signal唤醒了其中一个线程,则其它线程仍然在等待被唤醒然后再尝试获取相应的互斥锁,它们阻塞在条件变量上
- 如果有多个线程阻塞在条件变量上,并且pthread_cond_broadcast唤醒它们,则所有线程都开始竞争互斥锁,胜利者开始执行,失败者阻塞在互斥锁上
如果没有任何线程基于条件变量阻塞,则调用pthread_cond_broadcast不起作用。
6)销毁条件变量状态
pthread_cond_destroy用于销毁与 cv 所指向的条件变量相关联的任何状态,但是没有释放用来存储条件变量的空间。
7)初始化条件变量属性对象
条件变具有一些属性,通过修改这些属性可以控制条件变量的一些行为。
pthread_condattr_init用来将与该对象相关联的属性初始化为其缺省值。pthread_condattr_init的参数类型实际上是opaque的,其中包含一个由系统分配的属性对象。该函数执行过程中会为属性对象分配所需的内存,因而如果未通过pthread_condattr_destroy销毁条件变量属性对象时就会导致内存泄漏。对于条件变量属性对象,必须首先通过调用pthread_condattr_destroy将其销毁,才能重新初始化该对象。
8)删除条件变量属性对象
pthread_condattr_destroy用来销毁指定的条件变量属性对象,实际上它完成了释放由pthread_condattr_init分配的内存的过程。
9)设置/获取条件变量的范围属性
pthread_condattr_setpshared用来将条件变量的范围设置为进程专用(进程内)或系统范围内(进程间)。
范围属性的取值及其含义:
- PTHREAD_PROCESS_SHARED:具有该属性的条件变量可以在多个进程中的线程之间共享。
- PTHREAD_PROCESS_PRIVATE:只有创建本条件变量的线程所在的进程内的线程才能够使用该条件变量。该值是缺省值。
pthread_condattr_getpshared用来获取条件变量属性对象cattr的范围属性的当前值。
三、自旋锁
1.基本概念
自旋锁是SMP架构中的一种low-level的同步机制。
当线程A想要获取一把自选锁而该锁又被其它线程锁持有时,线程A会在一个循环中自选以检测锁是不是已经可用了。对于自选锁需要注意:
- 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在那里自旋,这就会浪费CPU时间。
- 持有自旋锁的线程在sleep之前应该释放自旋锁以便其它线程可以获得自旋锁。(在内核编程中,如果持有自旋锁的代码sleep了就可能导致整个系统挂起,最近刚解决了一个内核中的问题就是由于持有自旋锁时sleep了,然后导致所有的核全部挂起(是一个8核的CPU))
使用任何锁需要消耗系统资源(内存资源和CPU时间),这种资源消耗可以分为两类:
- 建立锁所需要的资源
- 当线程被阻塞时锁所需要的资源
对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。
对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。
因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。
2.API
POSIX定义的自旋锁的数据类型是: pthread_spinlock_t
相关API
#include <pthread.h>
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);成功返回0,其它返回值表示出错
int pthread_spin_lock(pthread_spinlock_t *lock);成功返回0,其它返回值表示出错
int pthread_spin_trylock(pthread_spinlock_t *lock);成功返回0,其它返回值表示出错
int pthread_spin_unlock(pthread_spinlock_t *lock);成功返回0,其它返回值表示出错
int pthread_spin_destroy(pthread_spinlock_t *lock);成功返回0,其它返回值表示出错
1)初始化自旋锁
pthread_spin_init用来申请使用自旋锁所需要的资源并且将它初始化为非锁定状态。pshared的取值及其含义:
- PTHREAD_PROCESS_SHARED:该自旋锁可以在多个进程中的线程之间共享。
- PTHREAD_PROCESS_PRIVATE:仅初始化本自旋锁的线程所在的进程内的线程才能够使用该自旋锁。
2)获得一个自旋锁
pthread_spin_lock用来获取(锁定)指定的自旋锁. 如果该自旋锁当前没有被其它线程所持有,则调用该函数的线程获得该自旋锁. 否则该函数在获得自旋锁之前不会返回。如果调用该函数的线程在调用该函数时已经持有了该自旋锁,则结果是不确定的。
3)尝试获取一个自旋锁
pthread_spin_trylock会尝试获取指定的自旋锁,如果无法获取则理解返回失败
4)释放(解锁)一个自旋锁
pthread_spin_unlock用于释放指定的自旋锁
5)销毁一个自旋锁
pthread_spin_destroy用来销毁指定的自旋锁并释放所有相关联的资源(所谓的所有指的是由pthread_spin_init自动申请的资源)在调用该函数之后如果没有调用pthread_spin_init重新初始化自旋锁,则任何尝试使用该锁的调用的结果都是未定义的。如果调用该函数时自旋锁正在被使用或者自旋锁未被初始化则结果是未定义的。