1、互斥量(Mutex)
1.1初始化
互斥量是属于pthread_mutex_t类型变量,使用之前必须初始化。
初始化方法有两种:静态初始化和动态初始化
静态初始化:pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER
pthread_mutex_t mtx是个结构体类型,静态初始化时,只能在定义时,不能先定义再用PTHREAD_MUTEX_INITIALIZER初始化。
比如: static pthread_mutex_t mtx;
Mtx = PTHREAD_MUTEX_INITIALIZER;
编译会报错。
#include <pthread.h> int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 成功:0 失败:非0 |
动态初始化:pthread_mutex_init
restrict修饰符,用来修饰一个指针,通俗讲:只要这个指针活着,我保证这个指针独享这片内存,没有‘别人’可以修改这个指针指向的这片内存,所有修改都得通过这个指针来。 |
原则上,在如下情况,应使用动态初始化:
- 动态分配于堆中的互斥量
- 互斥量是在栈中分配的自动变量
- 静态分配,但不使用默认属性的互斥量
- 静态分配,但没有在定义的时候静态初始化的
所以,使用静态初始化的情况如下:静态分配的变量,使用默认属性。包括:全局变量,局部static静态变量。
如下的静态初始化
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static struct vhost_user vhost_user = {
.fdset = {
.fd = { [0 ... MAX_FDS - 1] = {-1, NULL, NULL, NULL, 0} },
.fd_mutex = PTHREAD_MUTEX_INITIALIZER,
.num = 0
},
.vsocket_cnt = 0,
.mutex = PTHREAD_MUTEX_INITIALIZER,
};
互斥量属性
通常情况下,使用默认属性即可。静态初始化使用的就是默认属性。
动态初始化函数pthread_mutex_init的第二个参数,为NULL时,就是使用默认属性。
截取DPDK源码中的一段
struct vhost_user_reconnect_list {
struct vhost_user_reconnect_tailq_list head;
pthread_mutex_t mutex;
};
static int
vhost_user_reconnect_init(void)
{
int ret;
pthread_mutex_init(&reconn_list.mutex, NULL);
...
return ret;
}
1.2 获取和释放
获取互斥量:pthread_mutex_lock(pthread_mutex_t *mutex)
释放互斥量:pthread_mutex_unlock(pthread_mutex_t *mutex);
尝试获取互斥量:pthread_mutex_trylock(pthread_mutex_t *mutex);
#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); 成功:0 失败:非0 |
要锁定互斥量,在调用pthread_mutex_lock时需要指定互斥量。如果互斥量当前处于未锁定状态,该调用将锁定互斥量并立即返回。如果其他线程已经锁定了这一互斥量,那么该调用会一直阻塞,直到该互斥量被解锁。
Pthread_mutex_trylock函数调用在获取互斥量时,如果互斥量当前处于未锁定状态,跟pthread_mutex_lock一样,锁定互斥量并立即返回。如果其他线程已经锁定这一互斥量,该调用不会阻塞,会返回EBUSY错误。
如果发起pthread_mutex_lock调用的线程自身之前已经将目标互斥量锁定,此时会产生什么结果呢?这跟互斥量属性有关。关于Mutex属性我们暂不做过多说明。后续遇到实际情况再补充。这里简单说明一下几个互斥量类型(类型属于属性的一种)。
PTHREAD_MUTEX_NORMAL
该类型的互斥量不具备死锁自检功能。如线程试图对已经由自己锁定的互斥量加锁,会发生死锁。互斥量处于未锁定状态或者已经由其他线程锁定,对其解锁会导致不确定的结果。(Linux上会成功)
PTHREAD_MUTEX_ERRORCHECK
对此类互斥量的所有操作都会执行错误检查。上面说的几种情况,都会导致函数返回错误。这类互斥量运行起来比一般类型慢,可以作为调试工具用。
PTHREAD_MUTEX_RECURSIVE
递归互斥量属性。支持线程对已经由自己锁定的互斥量加锁。只要保证加锁的次数和解锁的次数匹配即可。解锁时如果互斥量处于未锁定状态,或者由其他线程锁定,操作会失败。
在Linux上,PTHREAD_MUTEX_DEFAULT类型互斥量的行为与PTHREAD_MUTEX_NORMAL相似。
如果由不止一个线程在等待获取由函数pthread_mutex_unlock解锁的互斥量,则无法判断究竟哪个线程将如愿以偿。
1.3 销毁
#include <pthread.h> int pthread_mutex_destroy(pthread_mutex_t *restrict mutex); 成功:0 失败:非0 |
这个销毁函数是与动态初始化函数配套使用的。静态初始化的Mutex不需要销毁操作。
只有当互斥量处于未锁定状态,且后续也无任何线程企图锁定它时,将其销毁才是安全的。
若互斥量驻留在动态分配的一片内存区域中,应在free此内存区域之前将其销毁。对于自动分配的互斥量(一般指局部非静态变量),也应在宿主函数返回前将其销毁。
经由pthread_mutex_detroy销毁的互斥量,可以用pthread_mutex_init对其重新初始化。
1.4死锁
有人的地方就有江湖。有锁的地方,就有死锁的风险。
一种典型的死锁情况:
线程A和B都成功锁住了一个互斥量,接着试图对已经被另一个线程锁定的互斥量加锁。两个线程无限制的等待下去。
另一种死锁情况:忘记释放锁。
一般在有goto, return的地方,容易忽略unlock。
1.5适用场景
因为Mutex是有名字的,可以找得到。所以互斥量可以应用在统一进程的不同线程之间,也可以用在不同进程的线程之间,也可以用在进程之间。
如果跨进程使用,互斥量需要是进程间共享的。注意条件有:
- 需要放在共享内存中,但不能放在reserve预留内存中。
- 要调用pthread_mutex_setpshared接口来设置进程间共享属性。
- 当一个进程异常退出了,没有释放mutex。怎么办?可以通过设置属性,获取锁时,发现锁被持有并且锁的owner不存在了,就会返回特定错误值EOWNERDEAD。获取错误值后,一系列操作来处理错误值:
- 首先调用pthread_mutex_consistent_np函数来恢复该锁的一致性
- 然后调用pthread_mutex_unlock来解锁
- 接下来在调用加锁
所以,跨进程使用Mutex比较复杂。遇到实际案例再做补充。