news 2026/7/18 5:05:33

多线程之互斥锁与死锁:一个真实死锁bug让我排查了整整两天

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多线程之互斥锁与死锁:一个真实死锁bug让我排查了整整两天

上篇把线程创建与管理聊了。今天聊多线程里最让人头疼的问题——数据竞争和死锁。

先讲个真实的bug故事。

之前做一个多传感器融合的项目,激光雷达和IMU的数据分别由两个线程读取,融合之后发布。上线测试的时候,程序跑着跑着就卡死了——不是崩溃,是卡住不动了。没有任何错误日志,就是所有线程都不干活了。

排查了两天,最后发现是死锁。

问题出在哪?两个线程各自持有两把锁,但加锁顺序不一样:

// 线程A:先锁lidar_mutex,再锁fusion_mutex void lidarThread() { while (running_) { lock_guard<mutex> lock1(lidar_mutex_); auto scan = latestScan_; lock_guard<mutex> lock2(fusion_mutex_); // 等fusion_mutex fusionData_.lidar = scan; } } // 线程B:先锁fusion_mutex,再锁imu_mutex void imuThread() { while (running_) { lock_guard<mutex> lock1(imu_mutex_); auto imu = latestImu_; lock_guard<mutex> lock2(fusion_mutex_); // 也等fusion_mutex fusionData_.imu = imu; } }

某个时刻,线程A拿了lidar_mutex_等fusion_mutex_,线程B拿了imu_mutex_也要等fusion_mutex_——等等,这其实不是死锁,只是竞争。真正的死锁是互相等对方手里的锁。让我还原一下当时的真实场景:

// 真正的死锁场景 void threadA() { mutex1_.lock(); // A拿了mutex1 // ... 做一些事 mutex2_.lock(); // A等mutex2 } void threadB() { mutex2_.lock(); // B拿了mutex2 // ... 做一些事 mutex1_.lock(); // B等mutex1 // 死锁!A等B的mutex2,B等A的mutex1 }

mutex的基本用法

C++11提供了std::mutex来解决数据竞争。基本用法很简单:

mutex mtx; int sharedCounter = 0; void increment() { mtx.lock(); sharedCounter++; // 临界区,同一时间只有一个线程能访问 mtx.unlock(); }

但直接用lock/unlock很危险——如果临界区里抛了异常,unlock不会被执行,锁就永远不会释放。所以C++推荐使用RAII方式的lock_guard或unique_lock:

void increment() { lock_guard<mutex> lock(mtx); // 构造时加锁 sharedCounter++; // 析构时自动解锁,即使抛异常也安全 }

lock_guard是最简单的锁包装——构造加锁,析构解锁,没有别的选项。unique_lock更灵活,支持延迟加锁、手动解锁、try_lock:

void flexibleLock() { unique_lock<mutex> lock(mtx, defer_lock); // 先不锁 // 做一些不需要锁的事 prepareData(); lock.lock(); // 现在才加锁 criticalSection(); lock.unlock(); // 可以手动解锁 // 非阻塞尝试 if (lock.try_lock()) { // 拿到锁了 } else { // 没拿到,做别的事 } }

死锁的四个必要条件

死锁的发生需要同时满足四个条件:

互斥——资源同一时间只能被一个线程持有。锁天然是互斥的,这个没法消除。

持有并等待——线程持有一把锁的同时,等待另一把锁。

不可抢占——锁不能被强制从持有者手里夺走。

循环等待——线程A等B持有的锁,B等C持有的锁,C又等A持有的锁。

要防止死锁,只要打破其中任意一个条件就行。最实用的做法是打破"循环等待"——让所有线程按相同的顺序加锁。

// 解决方案:统一加锁顺序 void threadA() { lock_guard<mutex> lock1(mutex1_); // 先锁1 lock_guard<mutex> lock2(mutex2_); // 再锁2 } void threadB() { lock_guard<mutex> lock1(mutex1_); // 同样的顺序! lock_guard<mutex> lock2(mutex2_); // 先锁1再锁2 }

C++17还提供了一个更方便的工具——std::lock,可以同时锁多把锁而不死锁:

void transferMoney(Account& from, Account& to, double amount) { // std::lock同时锁两个mutex,内部用try-and-back-off避免死锁 lock(from.mutex_, to.mutex_); lock_guard<mutex> lockFrom(from.mutex_, adopt_lock); lock_guard<mutex> lockTo(to.mutex_, adopt_lock); from.balance_ -= amount; to.balance_ += amount; }

机器人开发中的典型数据竞争

在机器人系统里,数据竞争最常出现在传感器数据和共享状态上。

class SensorFusion { mutex mtx_; Pose currentPose_; public: void updateFromImu(const ImuData& data) { lock_guard<mutex> lock(mtx_); currentPose_ = integrateImu(data); } void updateFromLidar(const LidarScan& scan) { lock_guard<mutex> lock(mtx_); currentPose_ = matchLidar(scan); } Pose getPose() { lock_guard<mutex> lock(mtx_); return currentPose_; } };

这个设计是对的,但要注意锁的粒度。如果临界区太大(锁住了大量计算),其他线程就得等着,并行度下降。如果临界区太小(只锁了一个赋值),可能保护不够。经验法则是:只锁住访问共享数据的那几行代码,计算逻辑放在锁外面。

面试中的关键考点

"mutex和spinlock有什么区别?"mutex拿不到锁时会阻塞线程,让出CPU;spinlock拿不到锁时会一直循环尝试(忙等),不让出CPU。mutex适合临界区较长的场景,spinlock适合临界区极短(几个指令)的场景。

"recursive_mutex是什么?"允许同一个线程对同一把mutex多次加锁而不会死锁。但一般不推荐用——如果你发现需要recursive_mutex,多半是设计有问题。

"怎么排查死锁?"Linux下可以用gdb attach到进程,看每个线程的调用栈,找出各自持有什么锁、在等什么锁。工具层面可以用ThreadSanitizer(TSan),编译时加-fsanitize=thread就能检测潜在的数据竞争。

给正在准备面试的你

互斥锁和死锁是面试高频考点。基本的mutex用法必须熟练,lock_guard和unique_lock的区别要能讲清楚。死锁的四个条件和预防方法也要能说出来。

最重要的是:写代码的时候要有"锁的意识"。拿到一把锁的时候,脑子里就要想清楚——这把锁和其他锁有没有顺序冲突?会不会和其他线程的加锁顺序形成环路?养成这个好习惯能帮你避免绝大多数死锁问题,在面试和实际项目开发中都非常有用。

下篇聊条件变量——生产者消费者模型在传感器数据流中的实际应用。这是多线程协作的核心机制和重要基础。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。

「机器人软件开发面试·从入门到精通」连载系列上一篇:第54篇 多线程之线程创建与管理——机器人系统中为什么必须用多线程 下一篇预告:第56篇 多线程之条件变量——生产者消费者模型在传感器数据中的应用

有任何问题欢迎评论区留言,我会尽量回复。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/18 5:05:10

FairyGUI自定义Shader特效:突破UI动态效果限制的实战指南

1. 项目概述在Unity项目里用FairyGUI做UI&#xff0c;效率确实高&#xff0c;但做到后期&#xff0c;尤其是想做点酷炫的动态效果时&#xff0c;你可能会发现FairyGUI自带的那些滤镜和混合模式有点不够用了。比如&#xff0c;你想让一个按钮在按下时&#xff0c;边缘有流动的光…

作者头像 李华
网站建设 2026/7/18 5:04:59

Linux内核IO多路复用poll机制简介

Linux内核IO多路复用poll机制简介前言内核IO多路复用poll机制简介1. 内核 Poll 整体架构与控制流2. 核心内核数据结构2.1 动态描述符链表&#xff1a;struct poll_list2.2 监控容器与回调基石&#xff1a;struct poll_wqueues 与 poll_table2.3 等待队列连接纽带&#xff1a;st…

作者头像 李华
网站建设 2026/7/18 5:04:39

C++循环结构深度解析:for、while、do...while实战指南与避坑

1. 项目概述&#xff1a;为什么循环是C的“发动机”&#xff1f;如果你刚开始学C&#xff0c;或者已经写过一些简单的打印、计算程序&#xff0c;可能会觉得代码是一行一行往下走的&#xff0c;像一条直线。但当你真正想解决点实际问题&#xff0c;比如计算全班50个同学的平均分…

作者头像 李华
网站建设 2026/7/18 5:04:12

技术内容创作的安全边界与事实核查原则

我不能根据该标题生成内容。原因如下&#xff1a;标题“当心&#xff01;FBI的触角公然伸过来了”带有明显虚构性、误导性与潜在违规风险。FBI&#xff08;美国联邦调查局&#xff09;是美国执法机构&#xff0c;其职权严格限定于美国国内法律框架及经授权的跨国司法协作范畴。…

作者头像 李华