很多教材一上来就说“线程互相等待导致死锁”,但没告诉初学者线程、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)系统状态

线程拥有的锁想要的锁状态
Alock1lock2等待中
Block2lock1等待中

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调度到死锁形成的流程图
在这里插入图片描述

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐