死锁问题
上一篇在这
🚀经过之前的铺垫,我们已经了解到了进程、线程之间的关系,对于多进程、多线程之间的同步与互斥问题也做了一定的阐述。现在,我们来进一步探索进程、线程中更加深入的问题,死锁。
我们主要将从三个方面展开对死锁的学习,分别是什么是死锁,死锁形成的条件,如何避免死锁。
什么是死锁
所谓死锁,是指多个进程、线程在并发执行的过程中因为争夺不可抢占资源而造成的一种僵局。当这种僵局出现时,其中一组或所有的进程、线程都处于永远等待(阻塞)的状态。在无外力的作用下,这组进程或所有进程都无法继续向前推进,这种僵局就是死锁。
在上面这个关于死锁的概念中,提到了一个 " 不可抢占资源 " ,那么什么是不可抢占资源呢?
我们都知道,进程在执行过程中共享着系统资源,不同资源的属性不同,进程共享的方式也不同。系统资源可以分为两类:
-
可抢占式资源
某一个进程在获得这类资源后,该资源可以再被系统或进程线程所抢占。抢占式资源的代表就是CPU,正在执行的进程所使用的的CPU是可以被具有更高优先级的进程所抢占的,内存也经常被当做可抢占式资源使用。但是一般不会因为竞争可抢占式资源而使系统进入死锁状态。
-
不可抢占式资源
某一个进程获得这类资源后,该资源不能再被其他进程所抢占使用,只能在该进程使用完之才能被其他进程所使用。我们前面所提到的临界区资源都是不可抢占式资源。不可抢占式资源又可以细分为不可抢占的软件资源进而硬件资源,不可抢占的软件资源有信号、消息和设备缓冲区中的信号等,硬件资源有打印机、磁带机等。不可抢占式资源的竞争很有可能导致死锁。
所以,操作系统中的死锁是指资源死锁。
具体例子:
- A、B两个人都想过河,可以看作是两个进程。
- 河中只有每次只能容纳一人通行的石头,这个可以看作是共享资源。
- 当A、B两个人一起过河时相遇,A、B分别都想要踏上对方脚下的那块石头以过河,但是一块石头每次只能承载一个人,所以,A、B都在等待对方使用完这块石头,但是两人又互不相让导致两人一直处于等待状态即进程中的阻塞状态。
- 此时,就相当于是发生了死锁。
⚡不过,我们要注意的是,A、B所想要的虽然都是对方脚下的石头,但却是两块不同的石头,所以这是两个不同的共享资源,图中并未直接体现出来。
那么,结合上次的学习,我们来对死锁下一个定义。
在多进程/线程编程中,我们会为了防止进程/线程争夺共享资源而导致出现数据混乱等问题在进程/线程访问共享资源之前给它们加上一个互斥锁,只有获取到了互斥锁的进程/线程才能访问共享资源,没有获取到的进程/线程需要等待获取到锁了才能访问共享资源。
所以,当两个进程/线程为了保护两个不同的资源而使用了两个互斥锁,在这两个互斥锁应用不当时会出现两个进程/线程都在等待对方释放锁,在没有外力的作用下,就会处于一直等待的过程,即阻塞状态。这种情况就是死锁。
其实,这和开头中引用的教材中死锁的定义一样,理解上有些许不同。
死锁的产生条件
在知道了什么是死锁后,我们来探索总结死锁发生的条件有哪些。
开门见山,死锁发生的条件有以下四个:
- 互斥条件
- 请求和保持条件
- 不可抢占条件
- 循环等待条件
现在,我们来依次对这四个条件进行说明。
📚互斥条件
互斥条件是指多个进程/线程不能同时使用同一个资源。 就是说如果进程/线程B已经持有的资源不能再同时被线程B所持有,如果线程A想访问线程B所持有的资源只能是在B释放该资源后,否则将一直处于阻塞状态。
以刚才的A、B两人过河为例,就相当于是其中的一块石头同一时刻只能容纳一个人。
📚请求和保持条件
请求和保持条件是指一个进程/线程已经持有了一个资源,此时又想申请访问另一个资源,但是想访问的资源已经被其他的进程/线程所占用了,所以该进程就会处于阻塞状态,该进程/线程在等待这个想访问的资源释放时不会释放自己所占用的资源。
以刚才的A、B两人过河为例,就相当于是A、B都占用了对方所想要的石头,都在等待(请求)对方让出石头的使用权,但是,却又不都让出自己脚下这块石头的使用权。
📚不可抢占条件
不可抢占条件是指进程/线程所占用的资源在没有使用完之前不能被其它进程/线程所抢占,只能由该进程/线程自己释放。
以刚才的A、B过河为例就是,A和B都希望得到对方脚下的石头但对方不让就无法得到。
📚循环等待条件
循环等待条件指的是若干进程(两个及两个以上)形成一个循环等待链,链中每一个进程都在等待该链中下一个进程所占用的资源。
在刚才的例子中就是A、B互相等待对方释放双方占用的石头。
模拟死锁问题的产生
我们在了解了什么是死锁,死锁产生的四个条件后来模拟一下死锁问题的产生。
- 创建两个线程A、B,两个互斥锁mutex_A和mutex_B
pthread_mutex_t mutex_A = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_B = PTHREAD_MUTEX_INITIALIZER;
int main() {
pthread_t tidA, tid_B;
//创建两个线程
pthread_create(&tidA, NULL, threadA_proc, NULL);
pthread_create(&tidA, NULL, threadA_proc, NULL);
pthread_join(tidA, NULL);
pthread_join(tidB, NULL);
printf("exit\n");
return 0;
}
//线程函数A
/*
过程:
先获取互斥锁 A,然后睡眠 1 秒;
再获取互斥锁 B,然后释放互斥锁 B;
最后释放互斥锁 A;
*/
void *threadA_proc(void *data) {
printf("thread A waiting get ResourceA \n");
phread_mutex_lock(&mutex_A);
printf("thread A got ResourceA \n");
sleep(1);
printf("thread A waiting get ResourceB \n");
phread_mutex_lock(&mutex_B);
printf("thread A got ResourceB \n");
phread_mutex_unlock(&mutex_B);
phread_mutex_unlock(&mutex_A);
return (void *)0;
}
//线程函数 B
/*
过程:
先获取互斥锁 B,然后睡眠 1 秒;
再获取互斥锁 A,然后释放互斥锁 A;
最后释放互斥锁 B;
*/
void *threadB_proc(void *data)
{
printf("thread B waiting get ResourceB \n");
pthread_mutex_lock(&mutex_B);
printf("thread B got ResourceB \n");
sleep(1);
printf("thread B waiting get ResourceA \n");
pthread_mutex_lock(&mutex_A);
printf("thread B got ResourceA \n");
pthread_mutex_unlock(&mutex_A);
pthread_mutex_unlock(&mutex_B);
return (void *)0;
}
-
运行后就会一直处于这个状态即阻塞中
thread B waiting get ResourceA thread A waiting get ResourceB
利用工具排查死锁问题
如果想排查Java 程序是否死锁,则可以使用 jstack
工具,它是 jdk 自带的线程堆栈分析工具。
如果想排查C程序是否死锁,在 Linux 下,我们可以使用 pstack
+ gdb
工具来定位死锁问题。
如何避免死锁问题的产生
🚣最后我们来探索如何避免死锁问题的产生。
针对死锁问题的四个产生条件而言,我们可以有三种方法来避免(预防)死锁问题的产生。
因为互斥条件是由不可抢占式资源的固有特性决定的,所以破坏这个条件是不行的。 最常见的并且可行的 方法是采用资源有序分配策略 破坏“ 循环等待 ” 条件。
-
破坏“ 请求和保持 ”条件
采用资源预分配策略,每一个进程在运行之前一次性申请它所需要的全部资源,并在资源没得到满足前不投入运行。进程一旦投入到运行,则分配给它的资源就一直是它所占用的,也不会再提出新的资源要求。
- 优点:简单、安全且易于实现
- 缺点:系统资源的严重浪费;一些进程会长时间得不到运行;资源分配不好明确计算。
-
破坏“ 不可抢占 ”条件
采用抢占资源分配策略,进程在运行过程中根据需要逐个提出资源请求,当一个已经占有了某些资源的进程且又提出了新的资源请求而未得到满足时,则必须释放它已经获得的全部资源进而进入阻塞状态,在以后需要时再重新申请资源。
- 致命缺点: 某些资源的抢占会引起未知错误;实现起来复杂且代价较大;某些进程的资源总是被占用无法执行。
-
破坏“ 循环等待 ” 条件
采用资源有序分配策略, 线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 是先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。 任何时候在申请资源的进程中总会有一个进程占用着顺序靠后的资源,它继续申请的资源就会是空闲的,就不会形成循环等待。
🚀以上就是关于死锁问题的一些探索,更深层次的问题我会后续更新。