news 2026/4/16 11:05:20

使用qthread编写第一个多线程程序:零基础示例

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用qthread编写第一个多线程程序:零基础示例

从零开始用 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 在每次运行时可能不同,但在同一次运行中保持一致。

你还可以自定义日志处理器,给每条日志打上线程标签,方便调试复杂系统。


小结:两条核心经验

  1. 永远不要阻塞主线程
    任何预计耗时超过 50ms 的操作都要移到子线程。

  2. 通信靠信号槽,别共享数据
    不要用全局变量或直接调用跨线程函数。用信号传递消息,由 Qt 自动处理线程切换。


下一步可以学什么?

掌握了QThread + moveToThread,你就迈过了 Qt 多线程的第一道门槛。接下来可以探索更高级的工具:

  • QtConcurrent::run():一键启动后台任务,适合一次性计算。
  • QThreadPool+QRunnable:管理一组可复用的工作线程,避免频繁创建销毁。
  • QFuture+QPromise:获取异步任务的结果,支持取消和进度查询。

这些工具建立在QThread的基础之上,理解底层原理才能用得更稳。


如果你现在就想动手试试,可以把上面的worker.hmain.cpp拷贝进一个新的 Qt Console 项目,编译运行,看看输出是否符合预期。

真正的掌握,始于亲手敲下第一行代码。

如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/16 10:49:28

Dify平台是否支持AMQP消息队列?异步解耦架构设计

Dify平台是否支持AMQP消息队列&#xff1f;异步解耦架构设计 在构建现代AI应用的实践中&#xff0c;一个越来越常见的挑战浮出水面&#xff1a;如何让像Dify这样以可视化编排为核心的LLM开发平台&#xff0c;在面对复杂、耗时的任务时依然保持响应灵敏和系统稳定&#xff1f;我…

作者头像 李华
网站建设 2026/4/15 16:30:02

FutureRestore-GUI终极教程:快速掌握iOS设备固件恢复的完整方案

FutureRestore-GUI终极教程&#xff1a;快速掌握iOS设备固件恢复的完整方案 【免费下载链接】FutureRestore-GUI A modern GUI for FutureRestore, with added features to make the process easier. 项目地址: https://gitcode.com/gh_mirrors/fu/FutureRestore-GUI 还…

作者头像 李华
网站建设 2026/4/15 19:53:41

Alkaid Mount:谐波驱动赤道仪技术架构与应用实践

Alkaid Mount&#xff1a;谐波驱动赤道仪技术架构与应用实践 【免费下载链接】AlkaidMount HarmonicDrive equatorial mount 项目地址: https://gitcode.com/gh_mirrors/al/AlkaidMount 项目概述 Alkaid Mount是一个基于谐波驱动技术的开源赤道仪系统&#xff0c;采用德…

作者头像 李华
网站建设 2026/4/8 19:04:43

高效方案:B站视频字幕智能提取与格式转换全流程

想要快速获取B站视频字幕进行学习或创作&#xff1f;BiliBiliCCSubtitle作为一款专业的免费字幕工具&#xff0c;能够轻松实现B站CC字幕的下载和SRT格式转换&#xff0c;让视频内容处理变得更加简单高效。 【免费下载链接】BiliBiliCCSubtitle 一个用于下载B站(哔哩哔哩)CC字幕…

作者头像 李华
网站建设 2026/4/9 18:52:13

终极bitsandbytes安装指南:从零配置到多平台部署

终极bitsandbytes安装指南&#xff1a;从零配置到多平台部署 【免费下载链接】bitsandbytes 8-bit CUDA functions for PyTorch 项目地址: https://gitcode.com/gh_mirrors/bi/bitsandbytes 想要在深度学习项目中实现高效计算和内存优化&#xff1f;bitsandbytes库正是您…

作者头像 李华
网站建设 2026/4/11 7:52:02

终极算子学习框架:DeepONet FNO完全指南

终极算子学习框架&#xff1a;DeepONet & FNO完全指南 【免费下载链接】deeponet-fno DeepONet & FNO (with practical extensions) 项目地址: https://gitcode.com/gh_mirrors/de/deeponet-fno DeepONet & FNO是一个强大的神经网络算子学习框架&#xff0c…

作者头像 李华