
Ⅰ. 死锁的概念
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
通常,死锁发生在多个进程同时需要相互占用对方正在使用的资源的情况下。这些资源可能是共享的,如内存、硬盘等,或者是独占的,如打印机、数据库等。当两个或多个进程同时请求某个共享资源,但这个资源已被其他进程占用时,这些进程就会被阻塞,等待资源被释放。但由于它们都在等待对方释放资源,因此它们最终会陷入无限等待的状态,从而形成死锁。
不仅仅是进程之间的问题,线程之间也会发生死锁的情况!更准确的说,死锁是发生在执行流之间的!
单个线程会不会发生死锁❓❓❓
答案是会的,因为一个线程要是多次加锁同样也会造成死锁问题,比如下面我们使用之前我们写过的代码,修改其中一部分:
void* thread_routine(void* args)
{
ThreadData* td = static_cast<ThreadData*>(args);
while(true)
{
pthread_mutex_lock(td->_mtx);
pthread_mutex_lock(td->_mtx); // 造成死锁!
if(ticket > 0)
{
usleep(12345);
cout << "new thread -> name: " << td->_name << ",the ticket: " << ticket << endl;
ticket--;
pthread_mutex_unlock(td->_mtx);
}
else
{
pthread_mutex_unlock(td->_mtx);
break;
}
usleep(1000);
}
}
有人可能会问,上面这种写法不是不可能写出来的吗,明显就是错的吗❓❓❓
是的,但是一般在大工程中,两个重复加锁的代码之间可能隔着很多行代码,导致疏忽的加锁,这都是有可能的,这都是 bug
。
Ⅱ. 死锁的四个必要条件
死锁的产生需要满足以下四个必要条件,也称为死锁的四个必要条件:
- 互斥条件(
Mutual Exclusion
):某种资源每次只能被一个执行流占用,如果该资源已经被占用,则其他执行流必须等待。 - 请求与保持条件(
Hold and Wait
):一个执行流在请求资源时,同时保持它已经占有的资源不放。 - 不剥夺条件(
No Preemption
):已经分配给执行流的资源不能被强制性地剥夺,只能由执行流自己释放。 - 环路等待条件(
Circular Wait
):系统中存在一个执行流资源的循环等待链,使得每个执行流都在等待下一个执行流所占用的资源。环路等待条件的产生,很大原因是因为所资源分配顺序不一致导致的。
当上述四个条件同时满足时,就有可能发生死锁。因此,为了避免死锁的发生,必须破坏其中至少一个条件。例如,可以采取资源预分配、避免持有多个资源、按固定顺序获取资源、以及使用超时等待机制等方法来避免死锁的产生。
Ⅲ. 避免死锁的方案
- 破坏死锁的四个必要条件中的其中一个以上条件。
- 加锁顺序一致。
- 避免锁未释放的场景。
- 资源一次性分配。
Ⅳ. 避免死锁的算法
-
银行家算法(
Banker's Algorithm
):银行家算法是一种避免死锁的算法(并非预防的方法),主要应用于多执行流共享资源的环境。银行家算法的思想是为了避免出现“环路等待”条件,它采用安全性检查的方式来避免系统进入不安全状态,从而保证资源的安全分配。银行家算法需要知道每个执行流对资源的最大需求量以及已经分配的资源量,根据这些信息预测系统是否处于安全状态。 -
资源分配图算法(
Resource Allocation Graph Algorithm
):资源分配图算法是一种图论算法,用于检测死锁以及避免死锁。该算法把执行流和资源分别表示为图的顶点,用有向边表示执行流对资源的请求以及资源的分配。通过检测是否存在环来判断系统是否存在死锁,如果存在,则通过破坏环中的一条边来解除死锁。 -
顺序资源分配法(
Ordered Resource Allocation
):该算法规定了一定的资源分配顺序,使得每个进程只有在获得当前所需的全部资源时,才能请求下一个资源。这样可以避免进程之间的循环等待,从而避免死锁的发生。 -
超时机制(
Timeout
):该算法通过设置超时时间来避免死锁。如果一个进程在一定时间内无法获取所需的资源,就会自动放弃这个请求,并释放已经占用的资源,从而避免死锁的发生。
此外,还有避免死锁的算法,如 资源排序算法(Resource Ordering Algorithm
)、独占和抢占算法(Mutual Exclusion and Preemption Algorithm
) 等。这些算法都旨在通过破坏死锁的四个必要条件中的一个或多个来避免死锁的发生。