Qt 客户端里的多线程,最核心的目的通常不是“提高吞吐量”,而是“不要把 UI 线程卡死”。
Qt 里为什么需要多线程?
Qt GUI 程序本质上有一个非常重要的线程:主线程 / GUI 线程。
像这些东西一般都跑在主线程:
QApplication::exec()事件循环- 鼠标、键盘事件
- 窗口刷新
- 按钮点击
paintEvent- 大部分 QWidget 操作
如果你在按钮槽函数里直接做一个耗时任务:
1 | void MainWindow::on_btn_clicked() |
这 10 秒期间,主线程一直被 doHeavyWork() 占着,Qt 的事件循环没法继续处理消息。
结果就是:
1 | 点击按钮 |
所以客户端多线程最典型的结构是:
1 | GUI 主线程 |
Qt 中常见的两种多线程方式
Qt 最核心的是 QThread。
但这里有个面试里很容易被问到的问题:
QThread 对象本身不等于“工作对象运行在这个线程”。
常见有两种写法。
第一种:继承 QThread
1 | class WorkerThread : public QThread |
使用:
1 | auto thread = new WorkerThread(this); |
start() 之后,Qt 创建新的线程,然后执行:
1 | WorkerThread::run() |
这种方式比较直观,适合:
- 一次性任务
- 简单后台计算
- 对事件循环没什么需求的线程
但是在实际 Qt 项目里,更推荐第二种。
推荐方式:QObject + moveToThread
定义 Worker:
1 | class Worker : public QObject |
然后:
1 | QThread* thread = new QThread; |
这里实际结构是:
1 | MainWindow Worker |
这也是 Qt 非常经典的:信号槽 + 工作线程模式。
原理:QObject 的线程归属
每个QObject内部记录了一个thread指针,代表这个对象依附于哪个线程的事件循环。
- 当信号槽使用 Qt::QueuedConnection(跨线程默认):信号不会直接调用槽函数,而是包装成
QEvent,丢到接收者对象所属线程的事件队列里,等该线程事件循环去执行。 moveToThread(obj, thread)修改的就是 obj 内部这个thread指针。
不是对象跑到另一个线程的栈 / 堆;对象内存地址不变。 而是:事件、队列槽函数,交给目标线程的 exec () 去调度。
为什么客户端特别适合这种模式?
因为客户端和服务器关注的点不一样。
可以这样对比:
| 客户端 | 服务器 | |
|---|---|---|
| 最核心目标 | UI 流畅、及时响应 | 吞吐量、并发量 |
| 主线程职责 | GUI 事件循环 | accept / event loop / 调度 |
| 多线程原因 | 防止耗时任务阻塞 UI | 同时处理大量请求 |
| 常见任务 | 文件加载、算法、推理、数据库、设备通信 | 网络连接、业务请求、CPU任务 |
| 常见模型 | GUI线程 + Worker线程 | 线程池 / Reactor / 协程 |
所以两边虽然都使用多线程,但是出发点明显不同。
Qt 客户端最常见的多线程场景
假设做一个 C++/Qt 工业软件,会非常容易碰到下面这些。
1. 大文件加载
例如打开一个大型模型:
1 | 点击“打开工程” |
错误做法:
1 | void MainWindow::openProject() |
如果这些全部在 GUI 线程:UI卡死 5秒
更合理的是:
1 | GUI线程 |
这在 Qt 工业客户端里非常常见。
2. 算法计算
例如:
1 | 有限元计算 |
假设:result = calculateFEM(mesh);需要 20 秒。如果放在主线程,客户端直接“假死”。
所以通常:
1 | UI线程 |
3. AI / 模型推理
比如工业视觉程序:
1 | 相机取图 |
推理可能一次需要几十毫秒甚至几百毫秒。如果直接放 GUI 线程:每推理一次 UI 就卡几十~几百 ms
特别是连续检测:30 FPS Camera 不断推理 UI 很容易彻底卡掉。
因此典型结构:
1 | GUI Thread |
例如:
1 | 相机线程:负责采图 |
4. 设备通信
工业客户端尤其常见。
比如:
1 | Qt 客户端 |
经常会有独立线程:
1 | GUI线程 |
例如 PLC:
1 | while (running) { |
GUI 只负责:
1 | void MainWindow::updatePLCData(Data data) |
5. 数据库 / 磁盘 IO
例如用户点击:打开历史数据需要查十万条数据库记录。如果直接:query.exec(...); 然后处理大量结果,UI 可能卡住。
因此也经常:
1 | GUI |
不过 Qt 有一个很重要的注意点:
数据库连接通常不能随便跨线程使用。
比如:QSqlDatabase 一般应该:哪个线程使用 → 哪个线程创建自己的连接;而不是主线程创建一个数据库连接,再扔给 Worker。
那网络 IO 一定要单独线程吗?
这里反而很值得注意。
不一定。
Qt 本身就是典型的事件驱动框架:
1 | QTcpSocket |
本身都是异步的。
例如:
1 | QNetworkReply* reply = manager->get(request); |
此时你并没有阻塞:
1 | GUI Thread |
所以:
不要看到网络请求就机械地创建一个线程。
这是 Qt 和很多传统同步服务器代码一个很大的区别。
Qt 中还有一个非常重要的概念:线程亲和性
Qt 的 QObject 有:
Thread Affinity
即一个 QObject 属于某个线程。
可以查看:
1 | obj->thread(); |
移动线程:
1 | worker->moveToThread(thread); |
例如:
1 | Worker |
那么它的槽函数通过 queued signal 被调用时:
1 | Thread B Event Loop |
这就是为什么:
1 | worker->moveToThread(thread); |
这么重要。
跨线程 signal-slot 是怎么工作的?
Qt 多线程最好理解的一点就是:
1 | Thread A Thread B |
也就是说跨线程时,通常不是:
1 | signal -> 直接调用 slot |
而是:
1 | signal |
这就是:
1 | Qt::QueuedConnection |
因此 Qt 很适合:
线程之间通过消息通信,而不是直接共享数据。
为什么 QWidget 不能在工作线程里操作?
这是 Qt 面试里非常常见的问题。
例如下面通常是不允许的:
1 | void Worker::doWork() |
原因是 QWidget 不是线程安全的,GUI 对象必须在 GUI 线程操作。
正确方式:
1 | emit progressChanged(50); |
然后:
1 | connect(worker, &Worker::progressChanged, |
执行路径:
1 | Worker Thread |
和服务器线程模型做一个最直观的比较
服务器可能是:
1 | Server |
核心问题:
1 | 怎么同时服务 10万连接? |
所以关心:
1 | 线程池 |
而 Qt 客户端更常见:
1 | GUI Thread |
核心问题变成:怎么保证 UI 始终流畅?
服务器端使用多线程,通常是为了提高并发处理能力和吞吐量;而 Qt 客户端使用多线程,最主要是把耗时操作从 GUI 主线程中剥离出去,避免阻塞事件循环。比如文件解析、算法计算、模型推理、相机采集、PLC 通信等工作通常放到工作线程,而 GUI 线程只负责界面响应和展示。线程之间尽量通过 Qt 的信号槽机制通信,避免工作线程直接操作 QWidget。