Linux环境下,C语言预防死锁的方法(参考APUE的11章以及12章)
一、死锁原因
1、用锁的原因
产生死锁,首先得用到锁,其原因为:
当一个线程修改变量时,其他线程在读取这个变量时可能会看到一个不一致的值。
在变量修改时间多于一个存储器访问周期的处理结构中,当存储器读与存储器写这两个周期交叉时,
这种不一致就会出现
2、产生死锁的原因
- 对已拥有的锁加锁。(apue 322页)
void main(void)
{
pthread_mutex_lock(&lock);
pthread_mutex_lock(&lock);
}
- 线程1拥有锁A,线程2拥有锁B。此时线程1想要锁B,线程2想要锁A。(apue 323页)
void * th_func1((void *)arg)
{
pthread_mutex_lock(&lock_A);
sleep(10);
pthread_mutex_lock(&lock_B);
}
void * th_func2((void *)arg)
{
pthread_mutex_lock(&lock_B);
sleep(10);
pthread_mutex_lock(&lock_A);
}
-
递归加锁问题
类似原因1
线程准备调用两个函数:func1()、func2()。而这两个函数都会用到1个以上的线程,且都需操作同一个数据结构。
因此我们把互斥锁嵌入该数据结构中,调用这两个函数后,都需要对互斥锁加锁。如果func1()调用func2()(互斥锁的属性不是递归类型)就会产生死锁。(apue 348页)
void main(void)
{
xxx *x;
func1(x);
func2(x);
}
void func1((xxx *)x)
{
pthread_mutex_lock(x->lock);
func2(x);
pthread_mutex_unlock(x->lock);
}
void func2((xxx *)x)
{
pthread_mutex_lock(x->lock);
pthread_mutex_unlock(x->lock);
}
- 线程获取锁后,未解锁就终止
线程在未解锁就终止退出的时候,其他线程获取该锁,就会出现死锁的情况
void main(void)
{
pthread_create(&th1,NULL,th_func,NULL);
sleep(1); //等待线程运行
pthread_mutex_lock(&lock);
}
void * th_func((void *)arg)
{
pthread_mutex_lock(&lock);
return (void*)0;
}
二、解决办法
1、避免对已拥有的锁又加锁(避免原因1)
apue 323页
2、所有的线程均按顺序加锁且一次性获取全部锁(避免原因2)
主要是从编写代码方面去避免。
apue 323页
pthread_t th1,th2;
pthread_mutex_t lock_A,lock_B;
void main(void)
{
pthread_create(&th1,NULL,func1,NULL);
pthread_create(&th2,NULL,func2,NULL);
}
void *func1((void*)arg)
{
pthread_mutex_lock(lock_A);
pthread_mutex_lock(lock_B);
sleep(10);
pthread_mutex_unlock(lock_A);
pthread_mutex_unlock(lock_B);
return (void *)0;
}
void *func2((void*)arg)
{
pthread_mutex_lock(lock_A);
pthread_mutex_lock(lock_B);
sleep(10);
pthread_mutex_unlock(lock_A);
pthread_mutex_unlock(lock_B);
return (void *)0;
}
3、使用绝对时间定时解锁的函数(避免原因1)
互斥锁
apue 327页
int pthread_mutex_timedlock(pthread_mutex_t *restrict __mutex, const struct timespec *restrict __abstime);
@ __mutex:
想要解锁的互斥锁地址
@ __abstime:
等待的绝对时间
return:
成功返回0,错误返回错误编号
读写锁
apue 332页
读锁
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict __rwlock, const struct timespec *restrict __abstime);
@__rwlock:
想要加读锁的读写锁地址
@__abstime:
等待的绝对时间
return:
成功返回0,错误返回错误编号
写锁
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict __rwlock, const struct timespec *restrict __abstime);
@__rwlock:
想要加写锁的读写锁地址
@__abstime:
等待的绝对时间
return:
成功返回0,错误返回错误编号
4、尝试解锁(避免原因1)
互斥锁
apue 321页
int pthread_mutex_trylock(pthread_mutex_t *__mutex);
@__mutex:
想要加锁的互斥锁地址
return:
成功返回0,错误返回错误编号。
该锁已被锁住,返回EBUSY
读写锁
apue 330页
读锁
int pthread_rwlock_tryrdlock(pthread_mutex_t *__rwlock);
@__rwlock:
想要加读锁的读写锁地址
return:
成功返回0,错误返回错误编号。
该锁已被锁住,返回EBUSY
写锁
int pthread_rwlock_trywrlock(pthread_mutex_t *__rwlock);
@__rwlock:
想要加写锁的读写锁地址
return:
成功返回0,错误返回错误编号。
该锁已被锁住,返回EBUSY
5、修改互斥锁的类型属性(避免原因1与原因3)
互斥锁有三种属性:进程共享属性、强壮属性、类型属性
apue 347页
首先先了解互斥锁的类型属性,一共有四种类型:
1、PTHREAD_MUTEX_NORMAL: 不做任何特殊的错误检测或死锁检测
2、PTHREAD_MUTEX_ERRORCHECK: 提供错误检测
3、PTHREAD_MUTEX_RECURSIVE: 允许多次递归的加锁,递归互斥锁维护锁计数,
解锁与加锁次数不匹配则不解锁 (方法7会详细举例说明)
4、PTHREAD_MUTEX_DEFAULT: linux默认为normal
互斥量类型 | 没有解锁时重新加锁? | 不占用时解锁? | 在已解锁时解锁? |
---|---|---|---|
PTHREAD_MUTEX_NORMAL | 死锁 | 未定义 | 未定义 |
PTHREAD_MUTEX_ERRORCHECK | 返回错误 值为35 | 返回错误 值为1 | 返回错误 值为1 |
PTHREAD_MUTEX_RECURSIVE | 允许 | 返回错误 值为1 | 返回错误 值为1 |
PTHREAD_MUTEX_DEFAULT | 未定义 | 未定义 | 未定义 |
注:
没有解锁时重新加锁: 也就是原因1,lock->lock
不占用时解锁: 线程1:lock 锁A,线程2:unlock 锁A
在已解锁时解锁: lock ->unlock ->unlock
我并未找到对应错误的返回值的宏定义名,知道的大佬可以评论区留言
int pthread_mutexattr_settype(pthread_mutexattr_t *__attr, int __kind);
@__mutex:
互斥锁属性地址
@__kind:
设置 互斥锁的类型属性
return
成功返回0,错误返回错误编
因此我们只需要将 互斥锁的类型属性 设置为错误检测属性PTHREAD_MUTEX_ERRORCHECK,在获取互斥锁后再获取锁就会报错,而不是死锁。
6、创建需要调用的不需要上锁的副本函数(避免原因3)
apue 349页
void main(void)
{
xxx *x;
func1(x);
func2(x);
}
void func1((xxx *)x)
{
pthread_mutex_lock(x->lock);
func2_locked(x); //func2的副本函数,不进行加锁解锁操作,避免递归加锁
pthread_mutex_unlock(x->lock);
}
void func2((xxx *)x)
{
pthread_mutex_lock(x->lock);
func2_locked(x);
pthread_mutex_unlock(x->lock);
}
void func2_locked(x)
{
...
}
7、设置递归互斥锁属性(避免原因3)
apue 351页
在定时执行任务的时候需要用到的方法。
例如:
接收到任务信息时,主线程需要对信息变量进行保护加锁,计算定时时间,创建线程定时执行任务函数(执行的任务函数也需要对互斥锁加锁)。
- 定时时间大于当前的时间,正常开启一个分离的线程。此时主线程解锁,新线程进行等待。到了时间后,执行需要执行的函数正常加解锁,不会产生死锁。
- 定时时间小于当前时间 或 动态分配内存失败 或 无法创建新线程,需要直接调用任务函数,此时主线程未解锁,新线程想加锁,就会产生递归加锁,出现死锁。
按方法5设置互斥锁的递归属性,就能避免递归加锁产生的死锁情况。
int pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
注意:
是调用不会死锁,而不是创建线程。创建线程仍然会产生死锁。
8、设置互斥锁强壮属性(避免原因4)
apue 346页
互斥锁的强壮属性取值:
1、PTHREAD_MUTEX_STALLED 设置stalled 则线程无法自动终止,且其他线程获取锁时会死锁。
2、PTHREAD_MUTEX_ROBUST 设置robust 其他线程获取该锁时,能通过判断加锁的函数返回值,
是否是EOWNERDEAD,来判断是否有该情况发生。并不会发生死锁情况
pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_DEFAULT);
虽然返回的EOWNERDEAD,但是已经获取到锁了,解锁前需要调用下面的函数,指明互斥锁相关状态在互斥锁解锁前是一致的。
int pthread_mutex_consistent(pthread_mutex_t *__mutex);
@__mutex:
互斥锁地址
return:
成功返回0,错误返回错误编号
如果未调用该函数就解锁,其他线程再次想加锁,则会返回错误ENOTRECOVERABLE,且该锁无法再次使用。