ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Qt 死锁避坑实录:从一段“看起来很 Qt”的代码说起

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 条比背《操作系统死锁四条件》有用:

  1. 锁永远不要跨 event loop 边界持有

  2. QueuedConnection 回调里不拿别的线程的锁

  3. 一个线程 = 一个 QMutex 责任域,不要“共享对象 + 共享锁”

  4. emit signal 前能 unlock 就 unlock

  5. 跨线程 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

返回列表