一、线程的多种使用方式
在 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()即可。 -
对象跨线程时需要注意清理顺序,避免悬空指针。示例中连接
finished到deleteLater可安全释放。 -
如果大量使用这种方式,每个线程内只运行一个工作对象,资源上类似继承方式,依然没有实现线程复用。
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::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() 获取。
示例:批量图像缩略图生成
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,无法直接发射信号。常见做法是:
-
多重继承 :同时继承
QObject和QRunnable(注意 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 登场了。