1. 死锁:四条件与破解之道
问:死锁是什么?发生死锁需要满足哪四个条件?
死锁是指两个或多个线程(进程)互相等待对方持有的资源,导致所有人都无法继续执行。
发生死锁必须同时满足以下四个条件:
| 条件 | 含义 |
|---|---|
| 互斥 | 资源一次只能被一个线程占用 |
| 请求与保持 | 线程占有一个资源,同时请求另一个资源 |
| 不可剥夺 | 已分配的资源不能被强行抢走 |
| 循环等待 | 线程间形成环形等待链 |
典型场景:
线程 A 线程 B
│ │
▼ ▼
锁住 X 锁住 Y
│ │
▼ ▼
请求 Y ←── 死锁 ──→ 请求 X
│ │
▼ ▼
等待 等待
问:怎么避免死锁?
| 策略 | 做法 |
|---|---|
| 预防 | 破坏四个条件之一。工程中最常用固定加锁顺序(破坏循环等待)——所有线程按同一顺序加锁 |
| 避免 | 银行家算法:分配前先试算,判断是否会导致死锁 |
| 检测与恢复 | 检测到循环等待后,终止进程或回滚 |
工程最佳实践:
// ❌ 危险:不同顺序加锁
void thread_a() {
pthread_mutex_lock(&lock1);
pthread_mutex_lock(&lock2); // 先 1 后 2
}
void thread_b() {
pthread_mutex_lock(&lock2);
pthread_mutex_lock(&lock1); // 先 2 后 1 → 可能死锁
}
// ✅ 安全:统一加锁顺序
void thread_a() {
pthread_mutex_lock(&lock1);
pthread_mutex_lock(&lock2); // 先 1 后 2
}
void thread_b() {
pthread_mutex_lock(&lock1); // 也先 1
pthread_mutex_lock(&lock2); // 后 2
}
如果无法固定顺序,可以用 pthread_mutex_trylock + 超时退避:
while (1) {
pthread_mutex_lock(&lock1);
if (pthread_mutex_trylock(&lock2) == 0) {
break; // 拿到两个锁
}
pthread_mutex_unlock(&lock1);
usleep(100); // 退避后重试
}
2. 条件变量与虚假唤醒
问:为什么 pthread_cond_wait 必须放在 while 循环里而不是 if?
因为存在虚假唤醒(spurious wakeup)——即使没有其他线程调用 pthread_cond_signal,pthread_cond_wait 也可能返回。
原因:
- 某些实现的内部机制
- 被系统信号中断
- 多核处理器下的竞争条件
// ❌ 错误:用 if
pthread_mutex_lock(&mtx);
if (queue_is_empty()) { // 只检查一次
pthread_cond_wait(&cv, &mtx);
}
// 虚假唤醒时队列可能还是空的,但这里直接取数据 → 出错
data = queue_pop();
pthread_mutex_unlock(&mtx);
// ✅ 正确:用 while
pthread_mutex_lock(&mtx);
while (queue_is_empty()) { // 每次唤醒都重新检查
pthread_cond_wait(&cv, &mtx);
}
data = queue_pop(); // 条件满足才执行
pthread_mutex_unlock(&mtx);
核心理解:while 确保条件满足才继续,不满足就继续等。这是条件变量的标准编程范式。
生产者-消费者完整示例:
// 生产者
void* producer(void* arg) {
while (1) {
pthread_mutex_lock(&mtx);
while (queue_is_full()) { // 满了就等
pthread_cond_wait(&full_cv, &mtx);
}
queue_push(data);
pthread_cond_signal(&empty_cv); // 通知消费者
pthread_mutex_unlock(&mtx);
}
}
// 消费者
void* consumer(void* arg) {
while (1) {
pthread_mutex_lock(&mtx);
while (queue_is_empty()) { // 空了就等
pthread_cond_wait(&empty_cv, &mtx);
}
data = queue_pop();
pthread_cond_signal(&full_cv); // 通知生产者
pthread_mutex_unlock(&mtx);
}
}
3. 优先级反转
问:什么是优先级反转?怎么解决?
现象:高优先级任务等待低优先级任务持有的资源,但低优先级任务被中等优先级任务抢占,导致高优先级反而等中等优先级。
高优先级任务 ──等待──┐
▼
低优先级任务 持有资源 ← 高优先级等低优先级
│
▼
中等优先级任务 抢占低优先级(不相关任务,但系统优先跑它)
│
▼
高优先级被阻塞,等低优先级跑完才能继续
解决方案:
| 方案 | 做法 |
|---|---|
| 优先级继承 | 低优先级任务持有资源时,临时提升到等待它的最高优先级 |
| 优先级天花板 | 每个资源预设一个优先级上限,获取资源时自动提升 |
优先级继承示例:
正常:高(100) > 中(50) > 低(20)
低持有锁 → 高等低 → 低临时提升到 100 → 低不会被中抢占 → 低释放锁 → 恢复优先级
4. 三种同步原语对比
问:互斥锁、条件变量、读写锁、自旋锁分别用在什么场景?
| 原语 | 场景 | 特点 |
|---|---|---|
| 互斥锁 (mutex) | 互斥访问临界区 | 谁加锁谁解锁,可能睡眠 |
| 条件变量 (cond) | 生产者-消费者模型 | 必须配合互斥锁使用 |
| 读写锁 (rwlock) | 读多写少 | 多读单写,读共享、写独占 |
| 自旋锁 (spinlock) | 临界区极短 | 忙等待,不睡眠,仅用于原子上下文 |
选择原则:
| 场景 | 推荐 |
|---|---|
| 临界区代码长,可能睡眠 | 互斥锁 |
| 需要等待某个条件满足 | 条件变量 |
| 读操作远多于写操作 | 读写锁 |
| 临界区极短(几条指令),或中断上下文 | 自旋锁 |
5. 中断与锁
问:中断里想和用户进程做同步,该用哪种锁?
必须用自旋锁。
原因:
| 锁类型 | 在中断中能否用 | 原因 |
|---|---|---|
| 互斥锁 | ❌ 不能 | 可能睡眠/调度,中断上下文没有进程上下文 |
| 读写锁 | ❌ 不能 | 可能睡眠 |
| 信号量 | ❌ 不能(down 可能睡眠) | 可能睡眠 |
| 自旋锁 | ✅ 能用 | 忙等待,不睡眠 |
中断中加锁的正确方式:
// ❌ 错误:中断中用互斥锁
void interrupt_handler() {
pthread_mutex_lock(&mtx); // 可能睡眠 → 崩溃
// ...
pthread_mutex_unlock(&mtx);
}
// ✅ 正确:中断中用自旋锁 + 关中断
void interrupt_handler() {
unsigned long flags;
spin_lock_irqsave(&lock, flags); // 关中断 + 加锁
// 临界区(极短)
spin_unlock_irqrestore(&lock, flags);
}
spin_lock_irqsave 做了两件事:
- 保存当前中断状态
- 关闭本地中断 + 加自旋锁
防止在持有自旋锁期间被同一个中断再次打断导致死锁。
6. 总结
线程同步知识图谱
│
├── 死锁(四条件)
│ ├── 互斥、请求与保持、不可剥夺、循环等待
│ └── 破解:固定加锁顺序、trylock+退避
│
├── 条件变量
│ ├── 必须配合 mutex 使用
│ └── while + wait 标准范式(防虚假唤醒)
│
├── 优先级反转
│ ├── 高优先级等低优先级,被中优先级抢占
│ └── 解决:优先级继承 / 优先级天花板
│
└── 锁的选择
├── 互斥锁 → 临界区长、会睡眠
├── 条件变量 → 生产者-消费者
├── 读写锁 → 读多写少
└── 自旋锁 → 临界区极短、中断上下文
核心理解:
- 死锁的本质是资源竞争,统一加锁顺序是工程中最简单有效的预防手段。
- 条件变量的正确用法是
while+pthread_cond_wait,虚假唤醒要求你每次唤醒后重新检查条件。 - 中断上下文只能用自旋锁,因为其他锁可能睡眠,而中断不能睡眠。
spin_lock_irqsave是标准写法。