如何避免线程发生死锁?

如何避免线程发生死锁?

C语言中文网:如何避免线程发生死锁?

前面章节,我们详细介绍了实现线程同步的 4 种方法,分别是互斥锁、信号量、条件变量和读写锁。很多初学者在使用这些方法的过程中,经常会发生“线程一直被阻塞”的情况,我们习惯将这种情况称为“死锁”。

线程死锁指的是线程需要使用的公共资源一直被其它线程占用,导致该线程一直处于“阻塞”状态,无法继续执行。举个例子,用互斥锁实现线程同步的过程中,初学者经常忘记为“加锁”的线程及时“解锁”,这种情况下就会发生死锁(实例一):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<stdio.h>
#include<pthread.h>
//创建并初始化互斥锁
pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;
void *thread_func(void *arg) {
int islock;
//为线程加锁
islock = pthread_mutex_lock(&myMutex);
if (islock == 0) {
printf("线程 %u 已加锁\n", pthread_self());
}
return 0;
}
int main() {
int flag;
int i;
//创建 4 个线程
pthread_t tids[4];
for (i = 0; i < 4; i++)
{
flag = pthread_create(&tids[i], NULL, thread_func, NULL);
if (flag == 0) {
printf("线程 %u 创建完成\n",tids[i]);
}
}
for(i = 0; i<4;i++){
pthread_join(tids[i], NULL);
printf("线程 %u 执行完成\n",tids[i]);
}
return 0;
}

假设程序编写在 thread.c 文件中,执行过程如下:

1
2
3
4
5
6
7
8
9
[root@localhost ~]# gcc thread.c -o thread.exe -lpthread
[root@localhost ~]# ./thread.exe
线程 3135751936 创建完成
线程 3125262080 创建完成
线程 3114772224 创建完成
线程 3135751936 已加锁
线程 3104282368 创建完成
线程 3135751936 执行完成
<-- 其它 3 个线程发生了死锁

程序中共创建了 4 个线程,它们都执行 thread_func() 函数,该函数内完成了对互斥锁的“加锁”操作,但没有调用 pthread_mutex_unlock() 函数对互斥锁“解锁”。通过程序的执行结果可以看到,4 个线程中仅有 1 个线程成功执行结束,其它 3 个线程一直处于等待互斥锁“解锁”的阻塞状态,发生了死锁。

再举一个例子(实例二):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;
pthread_mutex_t mutex2;
void *func1(void *args)
{
pthread_mutex_lock(&mutex);
printf("t1 成功申请 mytex 锁\n");
sleep(2);
pthread_mutex_lock(&mutex2);
printf("t1 成功申请 mytex2 锁\n");
printf("%u is running\n",pthread_self());
pthread_mutex_unlock(&mutex);
printf("------%u done\n",pthread_self());
}
void *func2(void *args)
{
pthread_mutex_lock(&mutex2);
printf("t2 成功申请 mytex2 锁\n");
sleep(2);
pthread_mutex_lock(&mutex);
printf("t2 成功申请 mytex 锁\n");
printf("%u is running\n",pthread_self());
pthread_mutex_unlock(&mutex);
printf("------%u done\n",pthread_self());
}
int main()
{
int ret;
pthread_t t1;
pthread_t t2;
pthread_mutex_init(&mutex,NULL);
pthread_mutex_init(&mutex2,NULL);
ret = pthread_create(&t1, NULL, func1, NULL);
if(ret != 0){
printf("create t1 fail\n");
}
ret = pthread_create(&t2, NULL, func2, NULL);
if(ret != 0){
printf("create t2 fail\n");
}

pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_mutex_destroy(&mutex);
pthread_mutex_destroy(&mutex2);
return 0;
}

假设程序编写在 thread.c 文件中,执行过程如下:

1
2
3
4
5
[root@localhost ~]# gcc thread.c -o thread.exe -lpthread
[root@localhost ~]# ./thread.exe
t1 成功申请 mytex 锁
t2 成功申请 mytex2 锁
<-- t1 和 t2 都发生了死锁

程序中创建了 mutex 和 mutex2 两个互斥锁,线程 t1 和 t2 同时执行。从执行结果可以看到,t1 成功申请了 mutex 锁,t2 成功申请了 mutex2 锁,t1 一直等待 t2 释放 mutex2 锁,而 t2 一直等待 t1 释放 mutex 锁,两个线程都因等待对方释放资源产生了死锁。

总的来说,当进程空间中的某公共资源不允许多个线程同时访问时,某线程访问公共资源后不及时释放资源,就很可能产生线程死锁。

就是最好不要锁中锁?锁了一个还没释放就又锁一个?

上面例子把 pthread_mutex_lock 修改为 pthread_mutex_trylock 就可以了。

修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;
pthread_mutex_t mutex2;
void *func1(void *args)
{
pthread_mutex_trylock(&mutex);
printf("t1 成功申请 mytex 锁\n");
sleep(2);
int islock = 0;
islock = pthread_mutex_trylock(&mutex2);
printf("t1 islock %d\n", islock);
if (islock == 0){
printf("t1 成功申请 mytex2 锁\n");
printf("t1 %u is running\n",pthread_self());
pthread_mutex_unlock(&mutex2);
}

pthread_mutex_unlock(&mutex);
printf("------t1 %u done\n",pthread_self());
}
void *func2(void *args)
{
pthread_mutex_trylock(&mutex2);
printf("t2 成功申请 mytex2 锁\n");
sleep(3);
int islock = 0;
islock = pthread_mutex_trylock(&mutex);
printf("t2 islock %d\n", islock);
if (islock == 0){
printf("t2 成功申请 mytex 锁\n");
printf("t2 %u is running\n",pthread_self());
pthread_mutex_unlock(&mutex);
}
pthread_mutex_unlock(&mutex2);

printf("------t2 %u done\n",pthread_self());
}
int main()
{
int ret;
pthread_t t1;
pthread_t t2;
pthread_mutex_init(&mutex,NULL);
pthread_mutex_init(&mutex2,NULL);
ret = pthread_create(&t1, NULL, func1, NULL);
if(ret != 0){
printf("create t1 fail\n");
}
ret = pthread_create(&t2, NULL, func2, NULL);
if(ret != 0){
printf("create t2 fail\n");
}

pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_mutex_destroy(&mutex);
pthread_mutex_destroy(&mutex2);
return 0;
}

避免线程死锁的几点建议

  1. 使用互斥锁、信号量、条件变量和读写锁实现线程同步时,要注意以下几点:
    • 占用互斥锁的线程,执行完成前必须及时解锁;
    • 通过 sem_wait() 函数占用信号量资源的线程,执行完成前必须调用 sem_post() 函数及时释放;
    • 当线程因 pthread_cond_wait() 函数被阻塞时,一定要保证有其它线程唤醒此线程;
    • 无论线程占用的是读锁还是写锁,都必须及时解锁。

注意,函数中可以设置多种结束执行的路径,但无论线程选择哪个路径结束执行,都要保证能够将占用的资源释放掉。

  1. POSIX 标准中,很多阻塞线程执行的函数都提供有 tryxxx() 和 timexxx() 两个版本,例如 pthread_mutex_lock() 和 pthread_mutex_trylock()、sem_wait() 和 sem_trywait()、pthread_cond_wait() 和 pthread_cond_timedwait() 等,它们可以完成同样的功能,但 tryxxx() 版本的函数不会阻塞线程,timexxx() 版本的函数不会一直阻塞线程。

    实际开发中,建议您==优先选择 tryxxx() 或者 timexxx() 版本的函数,可以大大降低线程产生死锁的概率。==

  2. ==多线程程序中,多个线程请求资源的顺序最好保持一致==。实例二中,线程 t1 先请求 mutex 锁然后再请求 mutex2 锁,而 t2 则是先请求 mutex2 锁然后再请求 mutex 锁,这就是典型的因“请求资源顺序不一致”导致发生了线程死锁的情况。