
前言Qt 的多线程设施建立在平台线程之上但它并不是简单地把std::thread包一层。QThread、事件循环、信号与槽的跨线程投递、QObject的线程亲和性thread affinity共同构成了一套有自己规则的模型。很多初学者把QThread当成线程句柄来用于是写出继承QThread再把耗时逻辑塞进槽函数的代码然后发现槽函数根本不在新线程里执行——这是对 Qt 线程模型最典型的误解。另一个常见误解是用了 Qt 的线程类就不会有数据竞争。事实恰恰相反Qt 提供的是同步原语和消息传递机制共享数据仍然要自己加锁跨线程直接读写同一个QObject的成员和裸std::thread一样是数据竞争按标准属于未定义行为undefined behavior, UB。本文先讲QThread的真实语义再说明两种正确用法子类化重写run()以及 worker 对象配合moveToThread()。随后讲跨线程信号槽的投递规则、常用同步原语最后给出一个结构完整的示例。文中以 Qt 5.15 与 Qt 6.x 的公共 API 为准若不同小版本有差异以 Qt 官方文档为准。示例需要 moc 处理带Q_OBJECT的类构建系统要开启 AUTOMOC。一、QThread 对象不等于线程本身必须先建立的一个事实是QThread的实例是一个普通的QObject它住在创建它的那个线程里只有run()的函数体执行在新创建的线程中。QThread::start()负责创建底层线程并调用run()run()的默认实现是调用exec()也就是启动该线程的事件循环。线程退出时发出finished()信号。由此可以推出两个结论。第一写在QThread子类构造函数里的代码运行在旧线程构造函数里new出来的QObject子对象也属于旧线程。第二给QThread子类添加的普通成员函数和槽函数默认同样在旧线程执行因为它们和对象本身绑在一起而不是和run()绑在一起。quit()的作用是让线程的事件循环以返回值 0 退出它不会强行终止正在执行的代码wait()阻塞调用者直到线程真正结束。terminate()会粗暴地终止线程线程中的资源可能来不及释放除非万不得已不要使用。成员运行所在线程说明构造函数、普通成员函数创建QThread的线程对象亲和性所在线程run()的函数体新线程默认实现调用exec()子类中定义的槽函数默认在对象所属线程除非对象被moveToThread()移走finished()信号在新线程发出投递给接收者连接方式决定最终执行线程二、两种正确用法2.1 worker 对象 moveToThread这是 Qt 官方更推荐的模式把耗时的逻辑放进一个继承自QObject的 worker 类创建后调用moveToThread()把它迁到目标线程再用信号启动它。此后该 worker 的槽函数在目标线程执行而它的生命周期仍由主线程管理。调用moveToThread()有几条硬性规则只能在对象当前所属的线程中调用对象不能有 parent因为子对象的线程亲和性跟随 parentQObject的子类如果含有定时器等与线程绑定的资源迁移时 Qt 会做相应处理但不要在两个线程里同时操作同一个对象。// 适用于 C17Qt 5.15 / Qt 6.x#include QCoreApplication#include QThread#include QDebugclass Worker : public QObject {Q_OBJECTpublic:explicit Worker(QObject *parent nullptr) : QObject(parent) {}public slots:void doWork() {// 此函数在 worker 所属线程即子线程执行for (int i 0; i 5; i) {QThread::msleep(100); // 模拟耗时操作emit progress(i * 20);}emit finished();}signals:void progress(int percent);void finished();};int main(int argc, char *argv[]) {QCoreApplication app(argc, argv);QThread thread;Worker *worker new Worker; // 不能设置 parentworker-moveToThread(thread);QObject::connect(thread, QThread::started,worker, Worker::doWork);QObject::connect(worker, Worker::finished,thread, QThread::quit);QObject::connect(thread, QThread::finished,worker, QObject::deleteLater);QObject::connect(worker, Worker::progress,app, [](int p) { qDebug() progress p; });QObject::connect(thread, QThread::finished,app, QCoreApplication::quit);thread.start();return app.exec();}这里有几个连接缺一不可started触发doWorkfinished让事件循环退出thread.finished负责删除 worker 并结束应用。progress的接收者是app它属于主线程因此这个连接自动变成队列连接槽函数在主线程执行打印是安全的。2.2 子类化 QThread 并重写 run如果线程只是一段独立的过程式任务不需要事件循环可以继承QThread并重写run()。此时耗时逻辑直接写在run()里而不是写在槽里。// 适用于 C17Qt 5.15 / Qt 6.xclass ComputeThread : public QThread {Q_OBJECTprotected:void run() override {long long sum 0;for (int i 1; i 1000000; i) {sum i;}emit resultReady(sum);// 没有调用 exec()线程执行完 run() 即结束}signals:void resultReady(long long value);};这种写法简单直接但缺点是不能接收排队的事件没有exec()就没有事件循环投递到该线程的队列连接不会被执行。若既想重写run()又想用事件驱动需要在run()末尾显式调用exec()。2.3 线程池与 QtConcurrent任务短小且数量多时反复创建线程并不划算。QThreadPool配合QRunnable可以复用线程QtConcurrent::run则把函数直接丢进全局线程池返回一个QFuture用于等待结果。需要注意QRunnable默认不归线程池所有是否自动删除由setAutoDelete(true)决定。三、跨线程信号槽的投递规则信号与槽的连接类型由Qt::ConnectionType控制。使用默认的Qt::AutoConnection时Qt 在信号发射的那一刻判断发送者所在线程与接收者所属线程相同就走直连direct不同就走队列连接queued。接收者的线程亲和性而不是信号的定义位置决定槽在哪里执行。队列连接的本质是参数被复制进一个事件投递到接收者线程的事件队列由该线程的事件循环取出后再调用槽。这带来两个要求参数类型必须能被 Qt 的元对象系统识别自定义类型需要qRegisterMetaType注册或在Q_DECLARE_METATYPE之后注册接收者线程必须正在运行事件循环否则事件永远排不上队。QMetaObject::invokeMethod可以在 Qt 5.10 及以上版本接受 lambda 和Qt::QueuedConnection用来把一段代码安全地丢到另一个线程执行。四、同步原语与共享数据Qt 提供的同步设施与标准库一一对应QMutex对应std::mutexQReadWriteLock对应读写锁QWaitCondition对应条件变量QSemaphore是计数信号量QAtomicInt、QAtomicPointer提供原子操作。Qt 6 中QMutexLocker是类模板用 CTAD 写成QMutexLocker locker(mutex);即可Qt 5 中它是普通类。使用条件变量时同样要搭配谓词循环防止虚假唤醒// 适用于 C17Qt 5.15 / Qt 6.xQMutex mutex;QWaitCondition cond;bool ready false;// 等待方{QMutexLocker locker(mutex);while (!ready) {cond.wait(mutex); // 内部会原子地释放并重新获取锁}}// 通知方{QMutexLocker locker(mutex);ready true;}cond.wakeAll();QAtomicInt只保证对这一个整数的读改写是原子的它不会让周围的其他数据自动获得同步语义。若要用原子变量做线程间的可见性保证需要理解内存序拿不准时使用默认的顺序一致性语义不要随意放松。常见坑点❌ 继承QThread后把耗时逻辑写进槽函数以为它在子线程执行。✅ 槽函数跟随对象亲和性仍在创建线程执行要么把逻辑写进run()要么用 worker moveToThread()。❌ 给 worker 设置 parent 后再调用moveToThread()。✅ 有 parent 的对象不能迁移线程worker 必须无 parent并在适当时机用deleteLater()释放。❌ 在子线程里直接调用QLabel::setText()等 GUI 接口。✅ 所有界面操作都放回主线程子线程通过队列连接发信号由主线程的槽更新界面。❌ 跨线程队列连接传递引用或裸指针对象在槽执行前已销毁。✅ 队列连接会复制参数传值或传智能指针确保接收方拿到的数据生命周期足够长。❌ 程序退出时QThread对象析构而线程仍在运行出现 QThread: Destroyed while thread is still running。✅ 退出前调用quit()再wait()或用requestInterruption()配合循环检查。❌ 用terminate()结束线程。✅ 它可能让互斥量停留在加锁状态、资源泄漏用标志位加wait()优雅退出。❌ 手动mutex.lock()之后中途return忘记解锁。✅ 用QMutexLockerQt 6 为模板做 RAII作用域结束自动解锁。❌ 多个互斥量按不同顺序加锁。✅ 全局约定固定顺序Qt 的QMutex没有标准库std::scoped_lock那样的多锁一次性接口必要时自己封装或用tryLock回退。总结主题关键结论QThread对象是QObject亲和性在创建线程只有run()在新线程两种用法过程式任务重写run()事件驱动用 worker moveToThread()连接类型AutoConnection在发射时按接收者亲和性决定直连或队列队列连接前提参数可元类型化接收线程有事件循环同步QMutex/QMutexLocker、QWaitCondition配谓词循环退出quit()wait()不要terminate()Qt 多线程的核心不是记住几个类名而是时刻清楚这段代码在哪个线程执行、这个对象属于哪个线程。把线程亲和性想明白信号槽的投递规则和加锁位置就都顺理成章了。