死锁的产生及常见造成死锁的情况
很多教材一上来就说“线程互相等待导致死锁”,但没告诉初学者线程、CPU、锁是怎么协同工作的,结果概念都悬空。那我现在就来从系统运行原理层面讲一遍,让你彻底理解“死锁”的发生逻辑。
一、CPU与线程的基础概念
1、CPU是“执行者”
(1)CPU(中央处理器)是计算机中唯一能实际运行指令的核心。
(2)你写的所有程序(线程、进程)都只是一堆等待执行的代码和数据。
(3)操作系统负责在 CPU 上调度这些程序轮流运行。
也就是说:
只有当前“占用CPU”的线程在真正执行,其他线程都在“等待调度”。
2、线程是程序执行的最小单位
(1)每个线程都有自己的运行栈、寄存器状态。
(2)当 CPU 切换线程时,会保存当前线程状态,然后恢复另一个线程的状态。
(3)这种切换叫上下文切换(Context Switch)。
举例说明:
时间片0ms-5ms: CPU执行线程A
时间片5ms-10ms: CPU切换执行线程B
时间片10ms-15ms: 又切换回A
所以你看到的“多个线程同时运行”其实是:CPU 在不同线程之间“快速切换”,造成“并行”的假象。
3、线程遇到“阻塞操作”会主动让出CPU
比如:
(1)等待IO(读文件、收网络包)
(2)等待锁(mutex)
(3)调用sleep()
这些操作都会让线程暂停运行,进入“等待状态(Blocked)”,操作系统就会把CPU让给其他线程使用。
二、加锁的作用:防止数据同时被改乱
假设两个线程要修改同一块内存:
count = 0;
线程1: count = count + 1;
线程2: count = count + 1;
在多核或多线程下,如果没有加锁,可能发生:
线程1 读取 count=0
线程2 读取 count=0
线程1 写入 count=1
线程2 写入 count=1
结果count=1,本应该是2,数据出错。所以我们使用锁(mutex)来保护:
pthread_mutex_lock(&lock);
count++;
pthread_mutex_unlock(&lock);
这样同一时刻只有一个线程能执行这段代码。
三、死锁是怎么产生的?
我们现在结合 CPU 调度来看死锁的全过程。假设有两个线程(A、B)和两把锁(lock1、lock2):
// 线程A
lock(lock1);
lock(lock2);
...
// 线程B
lock(lock2);
lock(lock1);
...
(1)CPU开始执行线程A
A:申请lock1 → 成功拿到
A:继续执行...(此时持有lock1)
(2)CPU时间片到了 —> 切换到线程B
B:申请lock2 → 成功拿到
B:继续执行...(此时持有lock2)
(3)CPU又切回线程A
A:申请lock2 → 失败(因为B正在持有)
A:阻塞(让出CPU,等待lock2释放)
(4)CPU切换到线程B
B:申请lock1 → 失败(因为A正在持有)
B:阻塞(让出CPU,等待lock1释放)
(5)系统状态
| 线程 | 拥有的锁 | 想要的锁 | 状态 |
|---|---|---|---|
| A | lock1 | lock2 | 等待中 |
| B | lock2 | lock1 | 等待中 |
A等B释放,B等A释放,两个线程都处于阻塞状态。操作系统看:这俩线程都在“等待资源”,但资源永远不会被释放。—— 就是死锁(Deadlock)。
举个生活例子理解:
| 人 | 手里拿的资源 | 想要的资源 |
|---|---|---|
| A | 筷子1 | 筷子2 |
| B | 筷子2 | 筷子1 |
两人都拿着一根筷子,然后都在等对方放下另一根。结果:
→ A 等 B,B 等 A
→ 谁也不放,永远卡住
四、CPU视角下死锁现象
CPU 轮流调度每个线程时,发现:
(1)A 线程:尝试获取 lock2 → 阻塞
(2)B 线程:尝试获取 lock1 → 阻塞
结果两个线程都进入“等待队列”,CPU只好去运行别的线程。如果别的线程也依赖这两个锁,就可能造成整个程序不再前进。
所以:死锁不是CPU卡死,而是线程们相互等待,谁也没法继续运行。
你可以这样理解:
a. 当线程申请锁时,如果拿不到锁 → 会进入“阻塞状态”;
b. 操作系统把CPU让给别的线程;
c. 但如果别的线程也在等前一个线程的锁 → 全部阻塞;
d. 操作系统即使不断切换,也找不到能继续执行的线程;
整个系统“表面还在运行”,但业务逻辑永远卡在原地。
五、死锁的“四个必要条件”
操作系统中,死锁的发生必须同时满足以下四个条件(称为“死锁四要素”):
| 条件 | 含义 | 举例 |
|---|---|---|
| 互斥(Mutual Exclusion) | 资源一次只能被一个线程使用 | 锁被某个线程持有后别人不能再拿 |
| 占有且等待(Hold and Wait) | 已占有资源的线程还要请求新的资源 | 线程1持有lockA,还想申请lockB |
| 不可剥夺(No Preemption) | 资源不能被强制剥夺,只能主动释放 | 锁只能自己解 |
| 循环等待(Circular Wait) | 存在一个循环等待链 | A→B→A 互相等 |
只要四个条件同时成立,就会产生死锁。
六、常见触发死锁的场景
(1)锁获取顺序不一致
多线程同时申请多个锁时,锁的顺序不统一最常见!例如以下情况:
// 线程1
lock(A);
lock(B);
// 线程2
lock(B);
lock(A);
死锁概率极高。解决办法:统一加锁顺序(比如总是先锁 A 再锁 B)。
(2)重复加锁(自己锁自己)
pthread_mutex_lock(&lock);
pthread_mutex_lock(&lock); // 死锁:线程自己等待自己释放锁
解决办法:使用“可重入锁”(如 recursive mutex)或避免重复锁。
(3)未正确释放锁(异常退出)
pthread_mutex_lock(&lock);
...
return; // 忘了解锁
其他线程永远拿不到锁。解决办法:使用 pthread_cleanup_push() 或 C++ RAII 自动释放。
(4)锁与信号量、条件变量混用不当
mutex_lock();
wait(semaphore); // 阻塞期间没释放 mutex
信号量等待中锁未释放,造成资源被永久占用。
(5)中断上下文里加锁
在 Linux 内核中,若中断或软中断与普通线程使用同一把锁,会导致:
a. 中断拿到锁;
b. 线程正在用锁;
c. 线程被中断;
d. 中断等待锁释放;
→ 系统卡死。
解决办法:中断中禁止获取可能被普通进程持有的锁。
(6)CPU调度到死锁形成的流程图

更多推荐
所有评论(0)