死锁以及预防
什么是死锁
死锁是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的互相等待现象。若无外力干涉,这些进程都无法向前推进。
经典比喻:哲学家就餐问题
- 五位哲学家围坐在圆桌旁,仅做思考和吃饭两件事。
- 桌上有五根筷子,每两位哲学家之间放一根。
- 哲学家需拿起左右两边的筷子才能吃饭。
- 若所有哲学家同时拿起左边的筷子,会永远等待右边筷子被放下,最终陷入死锁。
死锁产生的四个必要条件
四个条件必须同时满足,才会发生死锁,缺一不可。
- 互斥条件:一个资源每次只能被一个进程使用。
- 示例:打印机、锁,一次只能被一个线程持有。
- 请求与保持条件:进程因请求资源阻塞时,对已获得的资源保持不放。
- 示例:线程A持有锁1,申请锁2时不释放锁1。
- 不剥夺条件:进程已获得的资源,未使用完前不能被强行剥夺。
- 示例:不能强行将线程A持有的锁1抢给线程B。
- 循环等待条件:若干进程形成头尾相接的循环等待资源关系。
- 示例:线程A持有锁1、等待锁2;线程B持有锁2、等待锁1。
解决和预防死锁的策略
核心思路:破坏上述四个必要条件中的至少一个,即可避免死锁。
策略1:预防死锁(设计时杜绝死锁)
最有效、最根本的方法,通过规则设计提前规避死锁可能。
- 破坏“请求与保持”条件
- 一次性申请所有资源:进程运行前,需申请所需全部资源。
- 若无法满足所有需求,则不分配任何资源,让进程等待。
- 特点:简单易实现,但资源利用率低、浪费严重。
- 破坏“不剥夺”条件
- 允许抢占:进程持有部分资源却无法获取新资源时,需释放所有已持资源,后续重新申请。
- 实操方式:通过超时阻塞获取新资源,超时未获取则释放已有资源,重新发起申请。
- 特点:实现复杂,会增加上下文切换开销。
- 破坏“循环等待”条件(最常用、最实用)
- 资源有序分配法:给所有资源类型定义统一顺序,进程必须按递增顺序申请资源。
- 原理:强制申请顺序后,无法形成“A等B、B等A”的环路。
- 示例:锁L1、L2、L3的申请顺序规定为L1→L2→L3
- 合法:线程A(L1→L2)、线程B(L1→L3)
- 非法:线程C(先L3→再L1)
- 拓展代码示例:
// 按顺序申请锁的正确示范 pthread_mutex_lock(&L1); // 先申请低序锁 pthread_mutex_lock(&L2); // 再申请高序锁 // 业务逻辑处理 pthread_mutex_unlock(&L2); // 释放顺序与申请相反 pthread_mutex_unlock(&L1);
策略2:避免死锁(运行时动态防止)
系统分配资源前,先判断分配后是否进入“不安全状态”(可能发生死锁的状态),若会则拒绝分配。
- 经典算法:银行家算法
- 核心思想:模拟银行放贷逻辑,确保资源分配后系统仍能满足所有进程的最大需求,避免“坏账”(死锁)。
- 优点:资源利用率高于预防策略。
- 缺点:需提前预知进程最大资源需求(实际编程难实现)、算法本身有性能开销,仅用于教学或特定系统设计。
策略3:检测与恢复(放任死锁后处理)
不预防死锁,定期检测系统是否发生死锁,若发生则采取恢复措施。
- 死锁检测
- 系统维护“资源分配图”,记录所有资源的分配与请求情况。
- 定期调用算法检查图中是否存在环路,有环路则表明发生死锁。
- 死锁恢复
- 进程终止:
- 终止所有死锁进程:简单粗暴,但代价极高。
- 逐个终止进程:每次终止一个(优先级最低、代价最小的),释放资源直到打破死锁环。
- 资源抢占:
- 从死锁进程中抢占资源,分配给其他进程直到死锁解除。
- 需解决三个问题:选择牺牲哪个进程、如何回滚进程到安全状态、防止同一进程反复被牺牲(饥饿)。
- 进程终止:
线程并发中的死锁实例与解决方案
典型场景
线程持有一个或多个资源时被取消,且未释放资源,可能导致死锁。
方案1:使用取消处理函数
线程被取消时,先释放所有资源再退出,通过“压栈-出栈”机制实现。
- 核心函数:
#include <pthread.h> // 压栈:将取消处理函数存入栈中 void pthread_cleanup_push(void (*routine)(void *), void *arg); // routine:取消处理函数地址(函数指针) // arg:传递给取消处理函数的参数地址 // 出栈:弹出取消处理函数 void pthread_cleanup_pop(int execute); // execute:0=只弹出不执行,非0=弹出并执行 - 注意事项:
- 压栈与出栈函数必须配套使用。
- 压栈与出栈需在同一层代码块中。
- 完整示例:
#include <stdio.h> #include <pthread.h> #include <unistd.h> void Func1(void *arg) { printf("[1]执行取消处理函数,释放资源...\n"); } void Func2(void *arg) { printf("[2]执行取消处理函数,释放资源...\n"); } void Func3(void *arg) { printf("[3]执行取消处理函数,释放资源...\n"); } void *TaskFunc(void *arg) { // 压入取消处理函数(栈结构,先进后出) pthread_cleanup_push(Func1, NULL); pthread_cleanup_push(Func2, NULL); pthread_cleanup_push(Func3, NULL); while (1) { printf("线程运行中...\n"); sleep(1); } // 出栈(0=只弹出不执行,因while循环不会正常退出) pthread_cleanup_pop(0); pthread_cleanup_pop(0); pthread_cleanup_pop(0); } int main(int argc, char const *argv[]) { pthread_t tid; pthread_create(&tid, NULL, TaskFunc, NULL); sleep(3); // 让线程运行3秒 pthread_cancel(tid); // 发送取消请求 pthread_join(tid, NULL); // 等待线程退出 printf("目标线程已退出\n"); return 0; }
方案2:设置取消响应状态与类型
通过控制线程对取消请求的响应行为,避免资源未释放的情况。
- 核心函数:
#include <pthread.h> // 设置取消状态 int pthread_setcancelstate(int state, int *oldstate); // state:PTHREAD_CANCEL_ENABLE(允许取消,默认)、PTHREAD_CANCEL_DISABLE(禁止取消) // 设置取消类型 int pthread_setcanceltype(int type, int *oldtype); // type:PTHREAD_CANCEL_DEFERRED(延迟响应,默认)、PTHREAD_CANCEL_ASYNCHRONOUS(立即响应) - 关键代码示例:
void *TaskFunc(void *arg) { // 禁止接收取消请求,确保资源操作完成 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); int Num = 5; while (Num--) { // 关键业务逻辑(持有资源期间) printf("线程运行中...\n"); sleep(1); } // 完成资源操作后,允许接收取消请求 pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); while (1) { // 后续循环,可响应取消 printf("[2]线程运行中...\n"); } }
补充:取消点(Cancelation Points)
POSIX.1 规定的函数,若线程可取消且类型为延迟响应,收到取消请求后会在调用这些函数时触发取消。man 7 pthreads
- 核心取消点函数(部分):
accept()、aio_suspend()、clock_nanosleep()、close()、connect()、creat()、fcntl()(F_SETLKW)、fdatasync()、fsync()、getmsg()、getpmsg()、lockf()(F_LOCK)、mq_receive()、mq_send()、mq_timedreceive()、mq_timedsend()、msgrcv()、msgsnd() - 注意:立即响应类型(PTHREAD_CANCEL_ASYNCHRONOUS)无需等待取消点,收到请求后立即执行取消。
示例代码
void * TaskFunc( void * arg )
{
// 把取消类型设置为立即响应
// 立即响应 = 不需要等待取消点函数被调用
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS , NULL );
int Num = 5 ;
while(Num)
{
// 没有取消点
}
}
拓展知识:死锁常见场景与避坑技巧
常见死锁场景
- 嵌套锁使用不当:线程A持有锁1申请锁2,线程B持有锁2申请锁1。
- 资源释放顺序错误:申请顺序为L1→L2,释放顺序却为L2→L1(虽不必然死锁,但增加风险)。
- 线程池任务依赖:线程池中的任务互相等待对方持有的资源。
实际开发避坑技巧
- 使用智能锁:如C++的
std::lock_guard、std::unique_lock,自动释放资源,避免手动解锁遗漏。 - 限制锁持有时间:给锁申请设置超时时间,超时未获取则释放已有资源并重试。
- 减少锁粒度:将大锁拆分为多个小锁,降低资源竞争概率。
- 日志监控:记录锁的申请、持有、释放日志,便于排查潜在死锁问题。

浙公网安备 33010602011771号