支持作者新书,点击京东购买《Yocto项目实战教程:高效定制嵌入式Linux系统》
📘 本篇聚焦 死锁(Deadlock) 的本质机制、触发条件、经典场景和代码演示。通过模拟现实办公设备中的“打印机 + 扫描仪”资源争夺,让你彻底理解两个线程如何互相等待导致死锁,并掌握如何分析、预防与破除。
🔁 Day 18 回顾
在 Day 18 中,我们构建了一个响应式表单系统,协程异步验证用户输入并反馈结果。
主要内容包括:
- 协程机制结合 UI 表单事件
- 使用
co_await
实现自然表达的等待验证 - 多字段输入与验证反馈
这些为我们理解多线程控制结构与资源管理打下了基础。
🎯 今日目标:理解并解决 C++ 多线程中的死锁问题
学习模块 | 核心内容 |
---|---|
死锁定义与四要素 | 如何判断、何时发生 |
现实模拟:打印机+扫描仪 | 具象化死锁情景,提升理解 |
死锁代码演示 | 两个线程、两个资源、互等导致死锁 |
死锁预防与解决策略 | 四种方案:资源排序、尝试锁定、RAII、调度拆分 |
🧩 一、什么是死锁?
死锁是指两个或多个线程因互相等待对方持有的资源,而导致永远无法继续执行的现象。
✅ 四个必要条件(环环相扣):
- 互斥:资源一次只能被一个线程占用
- 占有并等待:线程已占有资源并继续请求其他资源
- 不可抢占:资源只能被主动释放
- 循环等待:线程之间形成环状资源等待链
📌 只要同时满足这四个条件,系统就可能进入死锁状态。
🔒 二、经典例子:打印机 + 扫描仪模型(具象化)
💡 场景设定:
- A 想先用 打印机,再用 扫描仪
- B 想先用 扫描仪,再用 打印机
如果 A 成功锁住了打印机,B 成功锁住了扫描仪,然后两人都想获取对方正在占用的资源,就会发生死锁。
💻 三、代码演示:两线程死锁实现
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex printer;
std::mutex scanner;
void personA() {
std::cout << "A 想用打印机\n";
printer.lock();
std::cout << "A 拿到打印机\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "A 想用扫描仪\n";
scanner.lock();
std::cout << "A 拿到扫描仪\n";
scanner.unlock();
printer.unlock();
std::cout << "A 完成任务\n";
}
void personB() {
std::cout << "B 想用扫描仪\n";
scanner.lock();
std::cout << "B 拿到扫描仪\n";
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "B 想用打印机\n";
printer.lock();
std::cout << "B 拿到打印机\n";
printer.unlock();
scanner.unlock();
std::cout << "B 完成任务\n";
}
int main() {
std::thread t1(personA);
std::thread t2(personB);
t1.join();
t2.join();
return 0;
}
🔍 输出(典型死锁):
A 想用打印机
A 拿到打印机
B 想用扫描仪
B 拿到扫描仪
A 想用扫描仪
B 想用打印机
(卡住)
❗ 四、分析死锁触发的根源
线程 | 第一步 | 第二步 |
---|---|---|
A | 锁住打印机 | 等待扫描仪 |
B | 锁住扫描仪 | 等待打印机 |
两个线程各持一个锁,又都想获得另一个锁 → 满足死锁的四个条件。
✅ 五、解决方案与最佳实践
✅ 方案一:统一加锁顺序(推荐)
void safePersonA() {
std::lock(printer, scanner);
std::lock_guard<std::mutex> lock1(printer, std::adopt_lock);
std::lock_guard<std::mutex> lock2(scanner, std::adopt_lock);
std::cout << "A 成功拿到所有资源\n";
}
📌 原理:所有线程按照相同顺序申请资源,不会形成“环状等待链”
✅ 方案二:使用 std::scoped_lock
(C++17)
void safePersonA() {
std::scoped_lock lock(printer, scanner);
std::cout << "A 成功拿到所有资源\n";
}
✅ 方案三:尝试加锁 + 超时退出(try_lock)
if (printer.try_lock()) {
if (scanner.try_lock()) {
// 成功
scanner.unlock();
}
printer.unlock();
}
✅ 方案四:RAII 资源控制(封装类自动释放)
- 始终用 lock_guard 或 scoped_lock
- 防止因为异常或 return 提前退出导致未释放
🧠 巩固练习题
Q1:什么是死锁?死锁产生的四个条件是什么?
A:互斥、占有并等待、不可抢占、循环等待
Q2:写出一个实际生活中的死锁例子?
A:两个银行账户互相转账时,各自锁住了自己的账户信息,等待对方 → 死锁。
Q3:如何避免死锁?
A:统一资源获取顺序、使用 scoped_lock、使用 try_lock、合理释放锁。
📘 购书推荐支持作者
📗 《Yocto 项目实战教程:高效定制嵌入式 Linux 系统》
👉 点击购买(京东自营)
📙 总结回顾
- 死锁本质是 多个线程之间形成“资源相互等待链”
- 必须理解四要素,才能准确分析问题
- 避免死锁 = 统一锁顺序、使用并发安全工具、释放要及时
- 通过打印机 + 扫描仪场景,深入理解“争夺 + 等待 + 不释放”的死锁过程
📌 期待你在实际项目中识别并解决死锁,让多线程系统更稳定可靠!