静态分配的互斥量
互斥量既可以像静态变量那样分配,也可以在运行时动态创建(例如,通过malloc()
在一块内存中分配)。这里先介绍静态分配,随后介绍动态分配。
互斥量是属于pthrad_mutex_t
类型的变量。在使用之前必须对其初始化。对于静态分配的互斥量而言,可如下所示,将PTHREAD_MUTEX_INITIALIZER
赋给变量:
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
加锁和解锁互斥量
初始化之后,互斥量处于未锁定状态。函数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);
Both return 0 on success, or a positive error number on error.
pthread_mutex_lock()
- 如果互斥量当前处于未锁定状态,
pthread_mutex_lock()
将锁定互斥量并立即返回。 - 如果其他线程已经锁定了这一互斥量,那么
pthread_lock()
会一直阻塞,直至该互斥量被解锁,调用将锁定该互斥量并返回。 - 如果发起
pthread_mutex_lock()
调用的线程自身之前已然将目标互斥量锁定,对于互斥量的默认类型而言,可能会产生两种后果——视具体实现而定:线程陷入死锁(deadlock),因为试图锁定已为自己所持有的互斥量而遭到阻塞;或者调用失败,返回EDEADLK
错误。在Linux上,默认情况下线程会发生死锁。
pthread_mutex_unlock()
pthread_mutex_unlock()
将解锁之前已遭调用线程锁定的互斥量,以下行为均属错误:
- 对处于未锁定状态的互斥量进行解锁。
- 解锁由其他线程锁定的互斥量。
如果有不止一个线程在等待获取由函数pthread_mutex_unlock()
解锁的互斥量,则无法判断究竟哪个线程如愿以偿。
pthread_mutex_trylock()和pthread_mutex_timedlock()
Pthread API提供了pthread_mutex_lock()
函数的两个变体:ptrhead_mutex_trylock()
和ptrhead_mutex_timelock()
。
- 如果信号量已然锁定,对其执行函数
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()
相继获得对互斥量的访问时,该线程将始终与此互斥量无缘。
动态初始化互斥量
静态初始值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 positive error number on error.
- 参数mutex
- 指定函数执行初始化操作的目标互斥量。
- 参数attr
- 是指向
pthread_mutexattr_t
类型对象的指针,该对象在函数调用之前已经过了初始化处理,用于定义互斥量的属性。 - 若将attr设为NULL,则该互斥量的各种属性会取默认值。
- 是指向
SUSv3规定,初始化一个业已初始化的互斥量将导致未定义的行为,应当避免这一行为。
以下情况必须使用pthread_mutex_init()
而非静态初始化互斥量。
- 动态分配于堆中的互斥量。
- 例如,动态创建针对某一结构的链表,表中每个结构都包含一个
pthread_mutex_t
类型的字段来存放互斥量,借以保护对该结构的访问。
- 例如,动态创建针对某一结构的链表,表中每个结构都包含一个
- 互斥量是在栈中分配的自动变量。
- 初始化经由静态分配,且不使用默认属性的互斥量。
(也即是,只有静态分配且使用默认属性的互斥量才用静态初始化。)
当不再需要经由自动或动态分配的互斥量时,应使用pthread_mutex_destroy()
将其销毁。(对于使用PHREAD_MUTEX_INTIALIZER
静态初始化的互斥量,无需调用pthread_mutex_destroy()
。)
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t * mutex);
Returns 0 on success, or a positive error number on error.
只有互斥量处于未锁定状态,且后续也无任何线程企图锁定它时,将其销毁才是安全的。
若互斥量驻留于动态分配的一片内存区域中,应在释放(free)此内存区域前将其销毁。对于自动分配的互斥量,也应在宿主函数返回前将其销毁。
经由pthread_mutex_destroy()
销毁的互斥量,可调用pthread_mutex_init()
对其重新初始化。
互斥量类型
前面的规则归纳如下:
- 同一线程不应对同一互斥量加锁两次。
- 线程不应对不为自己所拥有的互斥量解锁(亦即,尚未锁定互斥量)。
- 线程不应对一尚未锁定的互斥量做解锁动作。
准确地说,上述情况的结果取决于互斥量类型(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()函数所创建的互斥量,都属于此类型。
- Linux上,PTHREAD_MUTEX_DEFAULT类型互斥量的行为与PTHREAD_MUTEX_NORMAL类型相仿。
下例创建一个带有错误检查属性的互斥量:
pthread_mutex_t mtx;
pthread_mutexattr_t mtxAttr;
pthread_mutexattr_init(&mtxAttr);
pthread_mutexattr_settype(&mtxAttr, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutex_init(mtx, &mtxAttr);
pthread_mutexattr_destroy(&mtxAttr);