Qt 多线程编程 一、线程的多种使用方式在 Qt 中实现多线程并不是只有一种标准答案。根据任务类型和架构需求你至少有三种主流的线程使用方法。本文将逐一给出完整的示例并剖析它们的优势与不足。1. 继承 QThread重写 run()这是最直观、最传统的方式派生一个QThread子类然后把耗时逻辑放入run()函数。示例WorkerThread 计算斐波那契数列// workerthread.h #pragma once #include QThread class WorkerThread : public QThread { Q_OBJECT public: explicit WorkerThread(int n, QObject *parent nullptr) : QThread(parent), m_n(n) {} signals: void resultReady(int result); protected: void run() override { int res fibonacci(m_n); emit resultReady(res); } private: int m_n; int fibonacci(int n) { if (n 1) return n; return fibonacci(n - 1) fibonacci(n - 2); } };在主线程中使用auto *thread new WorkerThread(40, this); connect(thread, WorkerThread::resultReady, this, [](int res){ qDebug() Fibonacci result: res; }); connect(thread, QThread::finished, thread, QObject::deleteLater); thread-start(); // 启动线程优势概念简单直接将任务封装在子类中。run()内部是独立的执行流无需考虑事件循环问题。不足当需要在一个线程内处理多个不同类型的任务时这种一对一关系过于僵硬。QThread对象实际上不代表线程本身而是线程的“管理者”。如果错误地将槽函数连接到WorkerThread的槽槽函数会在主线程执行而不是在子线程中极易造成误解。正确做法是只有run()中的代码才运行在新线程里。这种“管理者与工作者不分”的设计一直是新手常犯的错误。2. 使用 QObject::moveToThread()更为灵活且符合 Qt 理念的方式创建一个普通的QObject工作类然后将其移入一个裸QThread。此时工作对象的所有槽函数都会在目标线程中执行。示例Worker 对象执行耗时操作// workerobject.h #pragma once #include QObject class WorkerObject : public QObject { Q_OBJECTpublic: explicit WorkerObject(QObject *parent nullptr) : QObject(parent) {} public slots: void doWork(int n) { int res fibonacci(n); emit resultReady(res); } signals: void resultReady(int result); private: int fibonacci(int n) { if (n 1) return n; return fibonacci(n - 1) fibonacci(n - 2); }};主线程调度QThread *thread new QThread(this); WorkerObject *worker new WorkerObject; // 无父对象 worker-moveToThread(thread); connect(thread, QThread::finished, worker, QObject::deleteLater); connect(this, MainWindow::startWork, worker, WorkerObject::doWork); connect(worker, WorkerObject::resultReady, this, [](int res){ qDebug() Result from worker thread: res; }); thread-start(); // 触发工作跨线程信号-槽 emit startWork(40);优势明确分离了“线程”与“工作任务”。QThread只管线程生命周期WorkerObject只管业务逻辑。可以利用信号-槽机制自然地在不同线程间通信支持多个槽函数并行处理如果线程内运行事件循环。易于将一个对象动态地移入或移出线程。不足必须启动线程的事件循环exec()默认启动否则槽函数无法被触发。QThread::run()默认调用exec()所以直接start()即可。对象跨线程时需要注意清理顺序避免悬空指针。示例中连接finished到deleteLater可安全释放。如果大量使用这种方式每个线程内只运行一个工作对象资源上类似继承方式依然没有实现线程复用。3. 使用 std::thread 与 QObject 结合在某些场景下你可能希望直接使用 C 标准库的std::thread同时仍然在子线程中使用 Qt 的事件系统如信号、槽、定时器等。这需要手动创建QEventLoop。示例std::thread 中运行 Qt 事件循环void startStdThreadWithEventLoop() { std::thread t([]{ // 在子线程中创建一个 QObject QObject worker; QTimer timer; timer.setInterval(1000); QObject::connect(timer, QTimer::timeout, []{ qDebug() Timer fired in std::thread, thread ID: QThread::currentThreadId(); }); timer.start(); // 必须启动事件循环否则 timer 不会触发 QEventLoop loop; loop.exec(); // 阻塞直到调用 quit }); t.detach(); // 或者 join()依实际情况 }优势完全使用标准 C 线程控制可以与现有的非 Qt 代码深度集成。可以更精细地控制线程的亲和性、调度策略等通过std::thread本地句柄。不足需要手动管理事件循环否则 Qt 的信号-槽、定时器等机制无法工作。跨线程信号-槽连接需要接收方有事件循环发送方的信号可能要求排队Qt::QueuedConnection容易出现遗漏。混合std::thread和QThread时必须警惕QThread::currentThreadId()的返回值不应混用QThread对象作为线程管理接口。不推荐作为常规方式更适用于已有 C 线程代码的移植。4. 几种基础方式的横向对比方式概念复杂度线程复用事件循环支持与信号/槽集成适用场景继承 QThread低否可选默认无困难易错单次、独立的后台任务moveToThread中否必须有极佳需要信号通信的常驻任务std::thread 事件循环高否需要手动启动复杂混合标准库与 Qt 的遗留代码可以看到这些方式都不具备线程复用能力。每执行一个任务就要创建并销毁线程频繁创建线程的开销不可忽视。为了解决这一问题我们需要线程池二、线程池之 QThreadPool 与 QRunnable线程池预先创建一组线程以任务队列的方式调度执行避免频繁创建/销毁线程的开销。Qt 提供了QThreadPool和QRunnable这套轻量级组合。1. QRunnable 与全局线程池QRunnable是一个抽象基类需要重写run()函数。任务可以被提交到QThreadPool后者自动调度执行。Qt 为每个应用程序维护了一个全局线程池通过QThreadPool::globalInstance()获取。示例批量图像缩略图生成// thumbnailsrunnable.h #pragma once #include QRunnable #include QImage class ThumbnailRunnable : public QRunnable { public: ThumbnailRunnable(const QString srcPath, const QString destPath, const QSize size) : m_srcPath(srcPath), m_destPath(destPath), m_size(size) {} void run() override { QImage img(m_srcPath); if (img.isNull()) { qWarning() Cannot load: m_srcPath; return; } QImage thumb img.scaled(m_size, Qt::KeepAspectRatio, Qt::SmoothTransformation); thumb.save(m_destPath); qDebug() Thumbnail saved to m_destPath; } private: QString m_srcPath; QString m_destPath; QSize m_size; };提交任务QThreadPool *pool QThreadPool::globalInstance(); pool-setMaxThreadCount(4); // 最大同时运行 4 个线程 for (int i 0; i imageFiles.size(); i) { auto *task new ThumbnailRunnable(imageFiles[i], thumbPaths[i], QSize(200, 200)); task-setAutoDelete(true); // 默认 true执行完自动销毁 pool-start(task); }全局线程池会自动排队执行任务无需手动管理线程。2. 独立线程池与优先级控制有时你需要隔离不同类型任务的线程资源。QThreadPool允许创建独立实例并可以设置任务优先级。QThreadPool *ioPool new QThreadPool(this); ioPool-setMaxThreadCount(2); // 给 I/O 密集任务保留 2 个线程 ioPool-setExpiryTimeout(3000); // 线程空闲 3 秒后退出 QRunnable *task new SomeIOTask(); task-setAutoDelete(true); ioPool-start(task, /*priority*/ 1); // 低优先级3. 与信号/槽的通信QRunnable不是QObject无法直接发射信号。常见做法是多重继承同时继承QObject和QRunnable注意 QObject 必须在第一基类。使用 QMetaObject::invokeMethod在主线程对象上调用方法。传递结果在任务完成后通过回调或QFuture见第三篇提交结果。示例可发送信号的 QRunnableclass NotifyRunnable : public QObject, public QRunnable { Q_OBJECT public: void run() override { // 模拟工作 QThread::sleep(2); emit finished(Task done); } signals: void finished(const QString message); };4. QThreadPool QRunnable 的优势与不足优势线程复用显著降低线程创建销毁的成本尤其适合短小任务。任务队列管理自动排队支持最大线程数控制防止过载。资源隔离可通过独立池为不同类型的任务保留专用线程。轻量简洁QRunnable接口极简适合无返回值的并发任务。不足无返回值/无进度QRunnable::run()返回 void不能直接获取任务执行结果或监控进度。信号/槽不便需要多重继承等技巧才能使用信号且线程安全要自行保证。无取消机制标准 API 不提供取消单个任务的方法只能通过全局clear()移除尚未开始的任务但无法中断正在执行的 run()。动态调整受限线程池大小可在运行中修改但已扩张的线程不会立即回收需等待超时。当我们需要获取返回值、控制任务取消、报告进度时就需要 QtConcurrent 登场了。