Qt多线程编程实践总结:QtConcurrent与moveToThread应用场景对比(可以说都是干货)
在使用Qt进行多线程开发时,常见的两种方式是 QtConcurrent::run 和 QObject::moveToThread。虽然它们都能实现“多线程”,但其底层机制、使用场景以及适用对象却大相径庭。本文结合实际开发经验,对 Qt 多线程机制的关键点进行总结,帮助开发者更合理地选择和使用这两种方式。代码详见https://gitee.com/zhang_jie_sc/LGHWTest
1.QtConcurrent::run:适合小任务、无状态操作
特点:
- 使用线程池 (QThreadPool) 自动管理线程;
- 每次调用 QtConcurrent::run(…),可能由不同线程执行;
- 不适合涉及成员变量或状态的对象方法;
- 非事件驱动,无法处理 Qt 信号槽、定时器等事件。
示例:
QtConcurrent::run(worker, &ImageWriterWorker::writeFrame, frame);
注意:
每次调用都可能在不同线程执行。
2.moveToThread + invokeMethod:适合长期运行的后台对象
特点:
- 将一个 QObject 派发到专门线程中;
- 使用事件循环异步处理信号和调用;
- 可处理 Qt 信号槽、事件、定时器等;
- 调用该对象的方法时,可通过 invokeMethod 安全切换至目标线程。
示例:
worker->moveToThread(thread);
QMetaObject::invokeMethod(worker, "writeFrame",Qt::QueuedConnection,Q_ARG(FrameData, frame));
所有调用都在 worker 所在线程中有序执行,适用于后台日志写入、图像处理、数据采集等任务。
3.线程归属
QObject 属于哪个线程,由创建它的线程决定;调用它的方法,则取决于调用方式。在C#的winform和wpf也是这样的,控件只能由创建它的线程进行操作,否则都是都过委托实现。
3.1. 对象的“线程归属”(线程亲缘性)
在 Qt 中,每个 QObject 都属于某一个线程 —— 即“线程亲缘性”(thread affinity):
- 默认情况下,QObject 在哪个线程中创建,就属于哪个线程;
- 可以通过 obj->moveToThread(targetThread) 显式更改其归属;
- 属于某个线程的对象,其槽函数在该线程的事件循环中执行,前提是使用了 QueuedConnection 或跨线程信号。
- 线程归属决定了事件处理的线程。
3.2. 方法调用行为
- 同线程直接调用: 立即在当前线程调用;
- 跨线程(Queued Connection): 方法调用变成一个事件,排入目标线程的事件队列;
- Auto Connection(默认): 若在同线程中,则为直接调用;否则自动变为队列调用。
如何从主线程调用后端对象的方法?
QObject::connect(sender, SIGNAL(...), receiver, SLOT(...), Qt::QueuedConnection);
或者:
QMetaObject::invokeMethod(obj, "someSlot", Qt::QueuedConnection, ...);
这样即使从主线程调用,也能保证目标函数在后台线程中执行,直接在主线程调用,会使程序出现不稳定状态。
3.3. 常见误区
错误做法 | 说明 |
---|---|
在主线程创建对象,moveToThread 后仍直接调用方法 | 仍然是在主线程执行代码,违背线程封装 |
不设置事件循环就 moveToThread | 无法处理信号、队列事件,线程形同虚设 |
QThread::start()默认会在线程中启动一个事件循环,正常调用就行了。
4.使用建议
场景 | 推荐方式 | 原因 |
---|---|---|
小量、一次性后台任务 | QtConcurrent::run | 简洁高效,适合无状态计算 |
长时间后台服务对象 | moveToThread+invokeMethod | 保证线程稳定性 |
对象需接收信号/定时器 | moveToThread | 线程需拥有事件循环 |
每次调用都能独立执行 | QtConcurrent::run | 线程池调度,性能佳 |
多线程操作有状态对象 | 避免 QtConcurrent | 存在线程安全问题 |
5.直接继承QThread为什么不被推荐
5.1.误解线程的角色
- QThread 的职责是管理线程的生命周期(创建、启动、停止),而不是直接承载业务逻辑。
- 很多人在继承 QThread 后,把业务逻辑写在 run() 里,这样线程内就不能再拥有事件循环(除非自己加 exec())。
5.2.没有事件循环,无法响应 signal-slot、定时器等机制
5.3.线程中的对象容易绑定到错误线程(还没验证,有兴趣可以验证一下)
如果你在 QThread 子类的 run() 中创建对象,这些对象自动归属于创建它们的线程(主线程),除非手动调用 moveToThread(),容易造成线程混乱或警告:QObject::startTimer: timers cannot be started from another thread。