Qt 多线程编程

一、线程的多种使用方式

在 Qt 中,实现多线程并不是只有一种标准答案。根据任务类型和架构需求,你至少有三种主流的线程使用方法。本文将逐一给出完整的示例,并剖析它们的优势与不足。

1. 继承 QThread,重写 run()

这是最直观、最传统的方式:派生一个 QThread 子类,然后把耗时逻辑放入 run() 函数。

示例:WorkerThread 计算斐波那契数列

cpp 复制代码
// 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);
     } };

在主线程中使用:

cpp 复制代码
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 对象执行耗时操作

cpp 复制代码
// 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);
    }};

主线程调度:

cpp 复制代码
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() 即可。

  • 对象跨线程时需要注意清理顺序,避免悬空指针。示例中连接 finisheddeleteLater 可安全释放。

  • 如果大量使用这种方式,每个线程内只运行一个工作对象,资源上类似继承方式,依然没有实现线程复用。

3. 使用 std::thread 与 QObject 结合

在某些场景下,你可能希望直接使用 C++ 标准库的 std::thread,同时仍然在子线程中使用 Qt 的事件系统(如信号、槽、定时器等)。这需要手动创建 QEventLoop

示例:std::thread 中运行 Qt 事件循环

cpp 复制代码
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::threadQThread 时,必须警惕 QThread::currentThreadId() 的返回值,不应混用 QThread 对象作为线程管理接口。

  • 不推荐作为常规方式,更适用于已有 C++ 线程代码的移植。

4. 几种基础方式的横向对比

方式 概念复杂度 线程复用 事件循环支持 与信号/槽集成 适用场景
继承 QThread 可选(默认无) 困难(易错) 单次、独立的后台任务
moveToThread 必须有 极佳 需要信号通信的常驻任务
std::thread + 事件循环 需要手动启动 复杂 混合标准库与 Qt 的遗留代码

可以看到,这些方式都不具备线程复用能力 。每执行一个任务,就要创建并销毁线程,频繁创建线程的开销不可忽视。为了解决这一问题,我们需要线程池

二、线程池之 QThreadPool 与 QRunnable

线程池预先创建一组线程,以任务队列的方式调度执行,避免频繁创建/销毁线程的开销。Qt 提供了 QThreadPoolQRunnable 这套轻量级组合。

1. QRunnable 与全局线程池

QRunnable 是一个抽象基类,需要重写 run() 函数。任务可以被提交到 QThreadPool,后者自动调度执行。Qt 为每个应用程序维护了一个全局线程池,通过 QThreadPool::globalInstance() 获取。

示例:批量图像缩略图生成

cpp 复制代码
// 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;
};

提交任务:

cpp 复制代码
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 允许创建独立实例,并可以设置任务优先级。

cpp 复制代码
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,无法直接发射信号。常见做法是:

  • 多重继承 :同时继承 QObjectQRunnable(注意 QObject 必须在第一基类)。

  • 使用 QMetaObject::invokeMethod 在主线程对象上调用方法。

  • 传递结果 :在任务完成后通过回调或 QFuture(见第三篇)提交结果。

示例:可发送信号的 QRunnable

cpp 复制代码
class 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 登场了。

相关推荐
用户805533698036 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner6 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz11 天前
QML Hello World 入门示例
qt
xcyxiner14 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner14 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner15 天前
DicomViewer (添加模型类)3
qt
xcyxiner15 天前
DicomViewer (目录调整) 2
qt
xcyxiner15 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00617 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术17 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript