Qt 死锁避坑实录:从一段“看起来很 Qt”的代码说起
目录
一、先给结论
二、一段“看起来完全没问题”的死锁代码
1.场景设定
2.死锁复现代码(精简版)
3.Worker 实现(死锁核心)
三、为什么直接死锁?(Qt 特有版)
1️. AutoConnection 在这里是 QueuedConnection
2️. 发生了什么?
四、更隐蔽的变种:deleteLater + 锁(Qt 老坑)
五、正确解法一(最推荐):emit 前解锁
原则:信号 = 通知,不是同步调用
六、解法二:明确 QueuedConnection + 拷贝数据(线程安全版)
七、解法三:用 QAtomic(轻量场景)
八、解法四:QObject 线程边界法则(高级)
一个对象只被一个线程“操作”
九、Qt 死锁预防清单
十、最终推荐模板
十一、一句话总结
觉得有用,就请您帮忙点赞转发收藏吧,您的鼓励是我创作的动力,多谢看官。
由于能力水平有限,文中的错误或不严谨的地方在所难免,还请批评指正。
摘要:Qt 多线程里 90% 的死锁都不是“写错了锁”,而是信号槽 + QObject::thread() + 隐式队列锁 + 递归 mutex 共同制造出来的。本文用一段真实感极强的 Qt 风格代码复现死锁,拆解 Qt 特有的Event Loop / QueuedConnection / 跨线程 delete / QMutex 递归陷阱,并给出 4 种可落地的工程级解决方案。看完你会明白:为什么
QMutexLocker救不了你,deleteLater()反而救了你。
一、先给结论
Qt 中预防死锁,记住这 5 条比背《操作系统死锁四条件》有用:
锁永远不要跨 event loop 边界持有
QueuedConnection 回调里不拿别的线程的锁
一个线程 = 一个 QMutex 责任域,不要“共享对象 + 共享锁”
emit signal 前能 unlock 就 unlock
跨线程 delete / access 对象 → 只用
deleteLater()
下面用代码说话。
二、一段“看起来完全没问题”的死锁代码
1.场景设定
Worker 在子线程干活
MainThread 发命令
用
QMutex保护共享数据用 signal/slot 通知状态
2.死锁复现代码(精简版)
// data.h struct SharedData { QMutex mutex; int value = 0; }; // worker.h class Worker : public QObject { Q_OBJECT public slots: void doWork(); signals: void progress(SharedData *data); }; // main thread SharedData data; Worker *worker = new Worker; QThread *th = new QThread; worker->moveToThread(th); connect(th, &QThread::started, worker, &Worker::doWork); connect(worker, &Worker::progress, [](SharedData *d){ d->mutex.lock(); // 子线程已经获取锁还在摸鱼没释放呢,主线程又想获取锁,相当于先后调用了两次lock(),程序必死锁 qDebug() << "UI read:" << d->value; d->mutex.unlock(); }); th->start();3.Worker 实现(死锁核心)
void Worker::doWork() { >三、为什么直接死锁?(Qt 特有版)1️. AutoConnection 在这里是 QueuedConnection
因为:
worker->moveToThread(th);
所以:
emit progress() → Worker(子线程) → MainThread → Qt 帮你 queue 一个 meta call
2️. 发生了什么?
执行顺序:
时间
线程
行为
T1
Worker
mutex.lock()
T2
Worker
emit progress()→ post event 到 main thread
T3
Main
event loop 处理 signal
T4
Main
lambda 里mutex.lock()阻塞
T5
Worker
等 main thread 处理完 signal
T6
Worker
永远等不到 mutex
经典死锁四条件 Qt 版达成:
互斥
占有且等待 (子线程占锁等 main)
不可抢占 (QMutex 默认)
循环等待 (main ↔ worker)
四、更隐蔽的变种:deleteLater + 锁(Qt 老坑)
void Worker::doWork() { QMutexLocker locker(&data->mutex); >五、正确解法一(最推荐):emit 前解锁原则:信号 = 通知,不是同步调用
void Worker::doWork() { { QMutexLocker locker(&data->mutex); >六、解法二:明确 QueuedConnection + 拷贝数据(线程安全版)signals: void progress(int value); // 简单数据,传值,不传指针
void Worker::doWork() { int v; { QMutexLocker locker(&data->mutex); >七、解法三:用 QAtomic(轻量场景)QAtomicInt value; void Worker::doWork() { emit progress(value.fetchAndAddRelaxed(10) + 10); }
无锁
不适合复杂结构
八、解法四:QObject 线程边界法则(高级)
一个对象只被一个线程“操作”
class Worker : public QObject { Q_OBJECT QMutex mutex; public: void safeAdd(); };
规则:
Worker 永远只在自己线程操作 mutex
UI 想读?→ signal / invokeMethod(Queued)
QMetaObject::invokeMethod(worker, []{ worker->safeAdd(); // ✅ 在自己线程跑 }, Qt::QueuedConnection);
本质:用 event loop 当锁
九、Qt 死锁预防清单
1️.QMutex 只保护数据,不保护逻辑
2️.emit signal 时,手里不能握着任何线程的锁
3️.AutoConnection 是 Qt 给你埋的雷,跨线程就当它是 Queued
4️.deleteLater() 是 Qt 线程安全最后的尊严
5️.event loop 能解决的同步,就别用 mutex
十、最终推荐模板
void Worker::doWork() { // 1. 锁最小作用域 int snapshot; { QMutexLocker locker(&m_mutex); m_data.value += 1; snapshot = m_data.value; } // 2. emit 无锁 emit progress(snapshot); // 3. 耗时操作 heavyTask(); }
十一、一句话总结
Qt 的死锁,大多不是多线程问题,而是你以为 Qt 帮你串行化了逻辑,其实它只是帮你排了个队。
真正的 Qt 并发安全,不是“锁住一切”,而是让对象只活在一个线程里,剩下的交给 signal / event loop / deleteLater。
