从零开始用 QThread 写多线程程序:一个真正能跑的入门示例
你有没有遇到过这样的情况?点击“加载文件”按钮后,整个界面卡住几秒钟,鼠标移上去连光标都变成了沙漏;或者在处理一段音频数据时,进度条纹丝不动,仿佛程序崩溃了?
这不是代码写得差,而是你把耗时操作放在了主线程里执行。现代 GUI 应用必须响应用户输入,而一旦主线程被长时间占用,就会导致“假死”——这正是多线程要解决的问题。
Qt 提供了多种并发编程方式,但对初学者来说,QThread是最直观、最容易上手的入口。今天我们就来写一个完整的、可运行的多线程小程序,带你真正理解QThread到底怎么用,以及为什么推荐使用moveToThread模式。
先看效果:我们要做什么?
设想这样一个场景:
- 主线程负责显示当前进度(比如一个模拟的进度条)。
- 子线程执行一个耗时任务:从 0 数到 100,每步延时 50ms。
- 每次更新数值时,子线程通过信号通知主线程刷新 UI。
- 任务完成后,自动退出并安全释放资源。
最终你会看到类似这样的输出:
Main thread ID: 0x12345678 [UI] Progress: 10 % Worker running in thread: 0x87654321 [UI] Progress: 20 % ... Task completed. Thread cleaned up.整个过程主线程始终畅通无阻,不会卡顿。
下面我们就一步步实现它,并解释每一个关键点背后的逻辑。
方法一:继承 QThread(简单但不推荐)
最直接的方式是让我们的工作类继承QThread,然后重写它的run()函数。
// workerthread.h #ifndef WORKERTHREAD_H #define WORKERTHREAD_H #include <QThread> #include <QDebug> class WorkerThread : public QThread { Q_OBJECT public: void run() override { for (int i = 0; i <= 100; ++i) { msleep(50); // 模拟耗时操作 emit progressUpdated(i); if (i % 10 == 0) qDebug() << "Working in thread:" << QThread::currentThreadId() << ", Progress:" << i << "%"; } emit finishedWork(); } signals: void progressUpdated(int percent); void finishedWork(); }; #endif // WORKERTHREAD_H主函数也很简洁:
// main.cpp #include <QCoreApplication> #include <QDebug> #include "workerthread.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); qDebug() << "Main thread ID:" << QThread::currentThreadId(); WorkerThread worker; QObject::connect(&worker, &WorkerThread::progressUpdated, [](int p) { qDebug() << "[UI Update] Progress:" << p << "%"; }); QObject::connect(&worker, &WorkerThread::finishedWork, [](){ qDebug() << "Task completed in worker thread."; QCoreApplication::quit(); }); worker.start(); // 启动新线程,自动调用 run() return app.exec(); }✅ 能跑吗?能。
🛑 推荐吗?不推荐长期使用。
为什么?因为这种方式违反了“单一职责原则”。QThread本应只是一个线程控制器,但现在却承担了“业务逻辑”的责任。更糟糕的是,如果你在这个类里加了一些槽函数,它们其实还是运行在主线程中!除非你显式地将对象移动到线程中,否则很容易踩坑。
所以 Qt 官方文档明确建议:不要继承 QThread,而是使用 moveToThread 模式。
方法二:moveToThread 模式(现代 Qt 的标准做法)
这才是你应该掌握的核心模式。
思路很简单:
- 创建一个普通的
QObject派生类(Worker),专门用来干活。 - 创建一个
QThread实例来管理线程生命周期。 - 把 Worker 对象“移动”到这个线程中:
worker->moveToThread(thread); - 然后通过信号和槽触发任务、传递结果。
第一步:定义 Worker 类
// worker.h #ifndef WORKER_H #define WORKER_H #include <QObject> #include <QThread> #include <QDebug> class Worker : public QObject { Q_OBJECT public slots: void doWork() { for (int i = 0; i <= 100; ++i) { QThread::msleep(50); emit progressUpdated(i); if (i % 10 == 0) qDebug() << "Worker running in thread:" << QThread::currentThreadId(); } emit finished(); } signals: void progressUpdated(int percent); void finished(); }; #endif // WORKER_H注意这里没有继承QThread,也没有run()方法。所有工作都在doWork()这个槽函数里完成。
第二步:在 main 中组织线程关系
// main.cpp(改进版) #include <QCoreApplication> #include <QThread> #include <QDebug> #include "worker.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); qDebug() << "Main thread ID:" << QThread::currentThreadId(); QThread* thread = new QThread; Worker* worker = new Worker; // 关键一步:将 worker 移动到子线程 worker->moveToThread(thread); // 当线程启动时,执行 doWork QObject::connect(thread, &QThread::started, worker, &Worker::doWork); // 任务完成时,退出线程 QObject::connect(worker, &Worker::finished, thread, &QThread::quit); // 线程结束时,自动删除 worker 和 thread QObject::connect(worker, &Worker::finished, worker, &QObject::deleteLater); QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); // 接收进度更新 QObject::connect(worker, &Worker::progressUpdated, [](int p){ qDebug() << "[UI] Progress:" << p << "%"; }); // 启动线程(触发 started 信号) thread->start(); // 可选:当线程结束后退出应用 QObject::connect(thread, &QThread::finished, &app, &QCoreApplication::quit); return app.exec(); }🔍 关键点解析
1.moveToThread()做了什么?
它改变了对象的“线程亲和性”(thread affinity)。从此以后,该对象的所有槽函数都会在目标线程中执行。这是实现跨线程任务调度的基础。
2. 为什么要连接finished → quit?
QThread::quit()是优雅退出事件循环的方式。如果不调用,线程会一直挂着,即使任务已经完成。
3. 为什么用deleteLater而不是直接delete?
因为worker正在另一个线程中运行,不能在主线程中直接销毁。deleteLater会发送一个事件到对应线程的事件循环,在安全时机自动清理内存,避免野指针或段错误。
4. 信号通信天然线程安全?
是的!当信号跨越线程发送时,Qt 默认使用Qt::QueuedConnection,意味着接收方的槽函数会在其所属线程的事件循环中被调用。这就保证了不会出现多个线程同时访问同一对象的情况。
常见误区与调试技巧
❌ 错误1:直接调用槽函数
worker->doWork(); // 危险!这会在当前线程同步执行!你以为是在子线程运行?错!只有通过信号触发或invokeMethod才能确保在正确线程执行。
✅ 正确做法:
QMetaObject::invokeMethod(worker, "doWork", Qt::QueuedConnection);或者更推荐的做法:发个信号让它自己启动。
❌ 错误2:忘记 quit() 导致线程无法退出
即使任务完成了,如果没调用thread->quit(),事件循环还在跑,线程就不会终止。
✅ 解决方案:始终连接finished → quit。
❌ 错误3:在子线程中操作 GUI
void Worker::doWork() { label->setText("Processing..."); // ❌ 绝对禁止! }GUI 组件只能在主线程中访问。任何 UI 更新都必须通过信号槽机制通知主线程去完成。
什么时候该用这种模式?
几乎所有涉及以下场景的应用都可以使用moveToThread:
| 场景 | 示例 |
|---|---|
| 文件读写 | 加载大文件时不卡界面 |
| 网络请求 | 发送 HTTP 请求并实时更新下载进度 |
| 图像处理 | 对图片进行滤镜、缩放等计算密集型操作 |
| 数据采集 | 工业控制中定时采集传感器数据 |
| 音视频编码 | 背景转码任务 |
只要你的操作可能超过几十毫秒,就应该考虑放到子线程中。
如何验证线程确实不同?
打印线程 ID 是最简单的办法:
qDebug() << "Current thread:" << QThread::currentThreadId();你会发现:
- 主线程的 ID 固定不变。
- 子线程的 ID 在每次运行时可能不同,但在同一次运行中保持一致。
你还可以自定义日志处理器,给每条日志打上线程标签,方便调试复杂系统。
小结:两条核心经验
永远不要阻塞主线程
任何预计耗时超过 50ms 的操作都要移到子线程。通信靠信号槽,别共享数据
不要用全局变量或直接调用跨线程函数。用信号传递消息,由 Qt 自动处理线程切换。
下一步可以学什么?
掌握了QThread + moveToThread,你就迈过了 Qt 多线程的第一道门槛。接下来可以探索更高级的工具:
QtConcurrent::run():一键启动后台任务,适合一次性计算。QThreadPool+QRunnable:管理一组可复用的工作线程,避免频繁创建销毁。QFuture+QPromise:获取异步任务的结果,支持取消和进度查询。
这些工具建立在QThread的基础之上,理解底层原理才能用得更稳。
如果你现在就想动手试试,可以把上面的worker.h和main.cpp拷贝进一个新的 Qt Console 项目,编译运行,看看输出是否符合预期。
真正的掌握,始于亲手敲下第一行代码。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。