C++每日训练 Day 19:死锁原理与实战剖析(打印机 + 扫描仪模型)

支持作者新书,点击京东购买《Yocto项目实战教程:高效定制嵌入式Linux系统》

📘 本篇聚焦 死锁(Deadlock) 的本质机制、触发条件、经典场景和代码演示。通过模拟现实办公设备中的“打印机 + 扫描仪”资源争夺,让你彻底理解两个线程如何互相等待导致死锁,并掌握如何分析、预防与破除。


🔁 Day 18 回顾

在 Day 18 中,我们构建了一个响应式表单系统,协程异步验证用户输入并反馈结果。

主要内容包括:

  • 协程机制结合 UI 表单事件
  • 使用 co_await 实现自然表达的等待验证
  • 多字段输入与验证反馈

这些为我们理解多线程控制结构与资源管理打下了基础。


🎯 今日目标:理解并解决 C++ 多线程中的死锁问题

学习模块核心内容
死锁定义与四要素如何判断、何时发生
现实模拟:打印机+扫描仪具象化死锁情景,提升理解
死锁代码演示两个线程、两个资源、互等导致死锁
死锁预防与解决策略四种方案:资源排序、尝试锁定、RAII、调度拆分

🧩 一、什么是死锁?

死锁是指两个或多个线程因互相等待对方持有的资源,而导致永远无法继续执行的现象。

✅ 四个必要条件(环环相扣):

  1. 互斥:资源一次只能被一个线程占用
  2. 占有并等待:线程已占有资源并继续请求其他资源
  3. 不可抢占:资源只能被主动释放
  4. 循环等待:线程之间形成环状资源等待链

📌 只要同时满足这四个条件,系统就可能进入死锁状态。


在这里插入图片描述

🔒 二、经典例子:打印机 + 扫描仪模型(具象化)

💡 场景设定:

  • 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 系统》
👉 点击购买(京东自营)


📙 总结回顾

  • 死锁本质是 多个线程之间形成“资源相互等待链”
  • 必须理解四要素,才能准确分析问题
  • 避免死锁 = 统一锁顺序、使用并发安全工具、释放要及时
  • 通过打印机 + 扫描仪场景,深入理解“争夺 + 等待 + 不释放”的死锁过程

📌 期待你在实际项目中识别并解决死锁,让多线程系统更稳定可靠!

支持作者新书,点击京东购买《Yocto项目实战教程:高效定制嵌入式Linux系统》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值