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 登场了。

相关推荐
whuhewei7 小时前
手写Promise
开发语言·javascript·ecmascript
AI科技星7 小时前
空间圆柱螺旋运动第一性原理终极推导·证明·核验·全量纲闭环
开发语言·人工智能·算法·计算机视觉·量子计算
basketball6167 小时前
C++ 多态完全指南:同一个接口,千变万化的行为
java·开发语言·c++
川冰ICE7 小时前
JavaScript入门⑤|数组方法全攻略,map/filter/reduce三剑客
开发语言·javascript·ecmascript
KANGBboy7 小时前
java知识二(程序流程控制)
java·开发语言
Evand J7 小时前
【MATLAB代码介绍】到达时间(TOA)定位,三维空间,带EKF的轨迹滤波与误差分析
开发语言·matlab
tongluowan0077 小时前
数据结构 Bitmap(位图)完整详解
开发语言·数据结构·bitmap
008爬虫实战录7 小时前
【码上爬】 题十八:模拟大厂加密算法, 堆栈分析找加密点,扣自执行函数,jsdom补环境
开发语言·javascript·ecmascript
skywalk81637 小时前
言知中文编程语言计划书 by WorkBuddy
开发语言·编程