Qt 多线程与并发编程详解
目录
前言
1. QThread 的正确用法:工作者-控制器模式
1.1 为什么不推荐继承 QThread?
1.2 正确模式:moveToThread()
1.3 安全地停止线程
2. 线程同步
2.1 QMutex (互斥锁)
2.2 QReadWriteLock (读写锁)
2.3 QSemaphore (信号量)
2.4 QWaitCondition (等待条件)
3. 线程间通信:信号和槽
连接类型 (Qt::ConnectionType)
4. QtConcurrent 框架
4.1 QtConcurrent::run()
4.2 QFuture 和 QFutureWatcher
总结
前言
在现代桌面应用程序开发中,用户界面的流畅响应至关重要。任何耗时的操作,如复杂计算、文件 I/O、网络请求等,如果直接在主线程(GUI 线程)中执行,都会导致界面冻结,严重影响用户体验。因此,掌握多线程编程是每一位 Qt 开发者的必备技能。
本文档旨在深入探讨 Qt 中多线程和并发编程的核心概念和最佳实践,帮助你构建响应流畅、稳定可靠的应用程序。
1. QThread 的正确用法:工作者-控制器模式
初学者最容易犯的错误就是继承 QThread
并重写其 run()
方法,将耗时操作放在其中。虽然这种方法可行,但它常常会导致对线程和对象所有权的误解。
核心理念:QThread
的主要职责是管理一个线程的生命周期和其事件循环,而不是作为执行耗时代码的容器。
推荐的、也是最能发挥 Qt 信号槽机制优势的模式,是将耗时任务封装在一个 QObject
子类(我们称之为“工作者”,Worker)中,然后将这个工作者对象“移动”到一个新的 QThread
实例所管理的线程中去执行。
1.1 为什么不推荐继承 QThread?
当你继承 QThread
并添加了自定义的槽函数时,这些槽函数以及对象本身实际上仍然属于创建该 QThread 对象的线程(通常是主线程),而不是 run()
方法执行所在的新线程。这意味着在 run()
之外调用这些槽函数,它们并不会在新线程中执行,这违背了多线程的初衷,并可能引发线程安全问题。
1.2 正确模式:moveToThread()
这种模式清晰地分离了线程管理和任务执行:
-
QThread (控制器): 负责启动、管理和销毁线程,并为线程提供一个事件循环。
-
QObject (工作者): 包含所有耗时任务的逻辑和数据,它的槽函数将在新线程中被执行。
实现步骤:
-
创建工作者类: 创建一个继承自
QObject
的类,将耗时操作封装成一个或多个公开的槽函数。 -
在主线程中设置和启动线程: 在主线程(例如
MainWindow
)中,创建QThread
和Worker
的实例,并建立它们之间的关系。
#include <QCoreApplication>
#include <QThread>
#include <QObject>
#include <QDebug>// 推荐的方式:创建一个继承自 QObject 的 Worker 类
class Worker : public QObject
{Q_OBJECTpublic:Worker(QObject *parent = nullptr) : QObject(parent){qDebug() << "Worker 构造函数所在的线程:" << QThread::currentThreadId();}~Worker(){qDebug() << "Worker 析构函数所在的线程:" << QThread::currentThreadId();}public slots:// 所有的耗时操作都放在这里void doLongRunningTask(){qDebug() << ">>>>>> Worker::doLongRunningTask() 所在的线程:" << QThread::currentThreadId() << ">>>>>>";qDebug() << "进入工作函数,开始执行耗时操作...";for (int i = 1; i <= 3; ++i) {qDebug() << "正在工作..." << i;QThread::sleep(1);}qDebug() << "工作完成!";emit workFinished(); // 发出完成信号}signals:void workFinished(); // 定义一个完成信号
};// 这是我们的控制器类,它负责管理线程和 Worker
class Controller : public QObject
{Q_OBJECTQThread workerThread; // QThread 对象作为成员变量public:Controller(QObject *parent = nullptr) : QObject(parent){Worker *worker = new Worker; // 1. 创建 Workerworker->moveToThread(&workerThread); // 2. 将 Worker 移动到新线程// 当线程启动时,触发 Worker 的工作函数connect(&workerThread, &QThread::started, worker, &Worker::doLongRunningTask);// 当 Worker 完成工作后,安全地退出线程connect(worker, &Worker::workFinished, &workerThread, &QThread::quit);// 当线程结束后,删除 Worker 对象connect(&workerThread, &QThread::finished, worker, &Worker::deleteLater);// 当线程结束后,也可以把 Controller 也删掉(如果需要)// connect(&workerThread, &QThread::finished, this, &Controller::deleteLater);qDebug() << "准备启动线程...";workerThread.start(); // 3. 启动线程}~Controller(){qDebug() << "Controller 析构";// 确保线程在 Controller 析构前已经停止workerThread.quit();workerThread.wait();}
};int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);qDebug() << "主线程 ID:" << QThread::currentThreadId();// 创建控制器,它会自动设置并启动一切Controller controller;// 当线程结束后,Qt的事件循环会自动处理对象的删除,然后程序可以退出// 为了在控制台程序中看到完整流程,我们连接线程的 finished 信号到程序的 quit// QObject::connect(&controller.workerThread, &QThread::finished, &a, &QCoreApplication::quit);// 上面这句会报错,因为 workerThread 是 private 的,这里只是为了说明// 在GUI程序中,你不需要手动退出,事件循环会一直运行return a.exec();
}#include "CorrectExample.moc"
```
**运行上述代码,您会得到类似下面的输出:**
```
主线程 ID: 0x...
Worker 构造函数所在的线程: 0x...
准备启动线程...
>>>>>> Worker::doLongRunningTask() 所在的线程: 0x... <-- 看这里!这是一个新的线程ID
进入工作函数,开始执行耗时操作...
正在工作... 1
正在工作... 2
正在工作... 3
工作完成!
Worker 析构函数所在的线程: 0x... <-- 在新线程中析构,非常安全
```
**结论非常清晰**:通过 `moveToThread`,我们成功地让 `Worker` 对象的所有逻辑(构造函数除外)都在一个新的、由 `QThread` 对象管理的线程中执行了。这才是 `QThread` 设计的初衷和最强大的用法。希望这两个例子和解释能够帮助您彻底理解 `QThread` 的正确使用方式!
1.3 安全地停止线程
强制终止线程 (QThread::terminate()
) 是非常危险的,它会立即结束线程,但不会执行任何清理代码(如释放内存、解锁互斥锁等),极易导致资源泄漏和死锁。
正确的停止方式:
-
设置一个标志位: 在工作者对象中设置一个
volatile bool
类型的标志位,例如m_abort
. -
在耗时任务中检查标志: 在循环或关键节点检查此标志位,如果为
true
则提前退出任务。 -
请求退出: 主线程通过调用一个槽函数来设置这个标志位为
true
。 -
调用
quit()
或exit()
: 这会请求线程的事件循环停止。如果线程正在执行耗时代码而没有返回事件循环,quit()
不会立即生效。 -
调用
wait()
: 等待线程完全执行完毕并退出。这可以确保在继续主线程逻辑之前,子线程已经完全清理干净。
#include <QCoreApplication>
#include <QThread>
#include <QObject>
#include <QDebug>
#include <atomic> // C++11原子操作库,用于线程安全的布尔标志// 工作者类,负责执行耗时任务
class Worker : public QObject
{Q_OBJECTprivate:// 1. 设置一个原子布尔类型的标志位。// std::atomic 可以保证多线程对它的读写操作是安全的,不会出现数据竞争。// 初始化为 false,表示不停止。std::atomic<bool> m_shouldStop{false};public:Worker(QObject *parent = nullptr) : QObject(parent) {}public slots:// 耗时任务的执行函数void doWork(){qDebug() << "工作者开始工作于线程:" << QThread::currentThreadId();int count = 0;// 2. 在耗时任务中(通常是循环)持续检查标志位while (!m_shouldStop){qDebug() << "工作中..." << count++;// 模拟耗时操作,例如读写文件、网络通信等QThread::msleep(500); // 休眠500毫秒}// 当循环结束后,意味着线程收到了停止请求并完成了任务qDebug() << "收到停止请求,工作循环结束。";emit workFinished(); // 发出工作完成信号}// 3. 提供一个公共的槽函数,用于从外部线程(如主线程)设置停止标志位void requestStop(){qDebug() << "主线程请求停止工作...";m_shouldStop = true;}signals:void workFinished(); // 工作完成信号
};int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);qDebug() << "主线程 ID:" << QThread::currentThreadId();QThread* thread = new QThread;Worker* worker = new Worker;worker->moveToThread(thread);// 当线程启动后,自动开始执行 doWork()QObject::connect(thread, &QThread::started, worker, &Worker::doWork);// 当工作者发出 workFinished 信号时,我们请求线程的事件循环退出// 4. 调用 quit() 来停止事件循环QObject::connect(worker, &Worker::workFinished, thread, &QThread::quit);// 当线程最终结束后,释放 worker 和 thread 的内存QObject::connect(thread, &QThread::finished, worker, &Worker::deleteLater);QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);QObject::connect(thread, &QThread::finished, [&](){qDebug() << "线程已安全结束。";});// 启动线程thread->start();// 创建一个定时器,在3秒后从主线程调用 requestStop() 来请求停止QTimer::singleShot(3000, [=](){// 这段代码在主线程中执行worker->requestStop();});// a.exec() 会启动主线程的事件循环,使得信号槽和定时器可以工作return a.exec();
}#include "SafeStopExample.moc"
2. 线程同步
当多个线程需要访问和修改同一个共享数据时,如果没有适当的保护,就会发生“竞态条件”(Race Condition),导致数据损坏或程序崩溃。Qt 提供了多种同步原语来解决这个问题。
2.1 QMutex (互斥锁)
QMutex
是最基本的同步工具,用于保护一个“临界区”(Critical Section),确保在任何时刻只有一个线程可以执行这段代码。
-
lock()
: 获取锁。如果锁已被其他线程持有,则当前线程会阻塞,直到锁被释放。 -
unlock()
: 释放锁。 -
tryLock()
: 尝试获取锁,如果失败则立即返回false
,不会阻塞。
最佳实践: 使用 QMutexLocker
,它利用了 C++ 的 RAII (Resource Acquisition Is Initialization) 技术。在构造时自动上锁,在析构时(离开作用域)自动解锁,从而避免了忘记解锁导致的死锁问题。
#include <QCoreApplication>
#include <QThread>
#include <QMutex>
#include <QDebug>
#include <QList>// --- 共享数据 ---
// 创建一个互斥锁实例,它将保护下面的共享计数器
QMutex mutex;
int sharedCounter = 0; // 这是所有线程都想访问和修改的共享资源// --- 线程任务 ---
// 这是一个自定义的线程类,它的任务是多次增加计数器
class WorkerThread : public QThread
{
public:// 线程启动后会自动执行 run() 函数void run() override{for (int i = 0; i < 5; ++i){// 这是关键部分:使用 QMutexLocker 来自动管理锁// 当代码执行到这一行时,QMutexLocker 的构造函数会自动调用 mutex.lock() 来获取锁。// 如果锁被其他线程占用,这个线程会在这里等待(阻塞)。QMutexLocker locker(&mutex);// ----- 临界区开始 -----// 在这里面的代码是受保护的,同一时间只有一个线程可以执行。sharedCounter++;qDebug() << this->objectName() << "将计数器增加到:" << sharedCounter;// ----- 临界区结束 -----// 当 locker 对象离开这个作用域(即 for 循环的一次迭代结束)时,// 它的析构函数会自动被调用,从而执行 mutex.unlock() 释放锁。// 这就是 RAII 技术,非常安全,可以避免忘记解锁。// 让线程稍微休眠一下,以便观察线程间的切换msleep(10);}}
};int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);qDebug() << "程序开始,初始计数器:" << sharedCounter;// 创建两个工作线程WorkerThread thread1;thread1.setObjectName("线程 A");WorkerThread thread2;thread2.setObjectName("线程 B");// 启动线程thread1.start();thread2.start();// 等待两个线程都执行完毕thread1.wait();thread2.wait();qDebug() << "所有线程执行完毕,最终计数器:" << sharedCounter;qDebug() << "期望的结果是 10 (每个线程增加5次)。如果没有锁,结果可能会小于10。";// a.exec(); // 对于控制台程序,可以不需要事件循环return 0;
}
2.2 QReadWriteLock (读写锁)
适用于“读多写少”的场景。它允许多个线程同时进行读操作,但写操作是互斥的。这比 QMutex
在读取频繁时效率更高。
-
lockForRead()
: 获取读锁。 -
lockForWrite()
: 获取写锁。写锁会阻塞所有其他读者和写者。 -
最佳实践: 相应地使用
QReadLocker
和QWriteLocker
。
#include <QCoreApplication>
#include <QThread>
#include <QReadWriteLock>
#include <QReadLocker>
#include <QWriteLocker>
#include <QDebug>
#include <QList>// --- 共享数据 ---
// 创建一个读写锁实例
QReadWriteLock rwLock;
// 假设这是一个共享的配置或数据,有很多线程会读取它,偶尔有线程会修改它
QString sharedMessage = "初始消息";// --- 读线程任务 ---
class ReaderThread : public QThread
{
public:void run() override{for (int i = 0; i < 3; ++i){// 使用 QReadLocker 获取读锁。// 多个读线程可以同时获取读锁,它们不会互相阻塞。QReadLocker locker(&rwLock);// ----- 读操作临界区 -----qDebug() << this->objectName() << "正在读取数据:" << sharedMessage;// ----- 临界区结束 -----// locker 离开作用域时自动释放读锁。msleep(15); // 休眠以观察效果}}
};// --- 写线程任务 ---
class WriterThread : public QThread
{
public:void run() override{for (int i = 0; i < 2; ++i){// 使用 QWriteLocker 获取写锁。// 当一个线程想要获取写锁时,它必须等待所有的读锁和写锁都被释放。// 一旦写锁被获取,其他任何线程(无论是读还是写)都必须等待。QWriteLocker locker(&rwLock);// ----- 写操作临界区 -----sharedMessage = QString("%1 写入新消息").arg(this->objectName());qDebug() << this->objectName() << "成功写入数据!";// ----- 临界区结束 -----// locker 离开作用域时自动释放写锁。msleep(25); // 休眠以观察效果}}
};int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);qDebug() << "--- 读写锁示例 ---";const int readerCount = 3;QList<ReaderThread*> readers;for(int i = 0; i < readerCount; ++i) {readers.append(new ReaderThread());readers.last()->setObjectName(QString("读线程 %1").arg(i + 1));}WriterThread writer1;writer1.setObjectName("写线程 A");// 启动所有线程for(ReaderThread* reader : readers) {reader->start();}writer1.start();// 等待所有线程执行完毕for(ReaderThread* reader : readers) {reader->wait();delete reader; // 释放内存}writer1.wait();qDebug() << "所有线程执行完毕。";qDebug() << "最终消息内容:" << sharedMessage;return 0;
}
2.3 QSemaphore (信号量)
信号量用于保护一定数量的相同资源。它可以看作是一个“广义的互斥锁”。一个初始化为 1 的信号量等价于一个互斥锁。
-
acquire(n)
: 获取 n 个资源。如果资源不足,线程会阻塞。 -
release(n)
: 释放 n 个资源,唤醒可能正在等待的线程。
一个经典的应用场景是控制生产者-消费者问题中的缓冲区大小。
#include <QCoreApplication>
#include <QThread>
#include <QSemaphore>
#include <QMutex>
#include <QQueue>
#include <QDebug>// --- 缓冲区大小 ---
const int BufferSize = 5; // 我们的缓冲区最多只能存放5个整数// --- 同步工具 ---
// 1. 信号量 freeSlots: 记录缓冲区中“空闲”位置的数量。
// 生产者在生产前需要获取一个空闲位置。初始时,所有位置都是空闲的。
QSemaphore freeSlots(BufferSize);// 2. 信号量 usedSlots: 记录缓冲区中“已使用”位置的数量。
// 消费者在消费前需要确保有已使用的位置。初始时,没有位置被使用。
QSemaphore usedSlots(0);// 3. 互斥锁: 保护对缓冲区队列本身的读写操作。
// 因为 QQueue 本身不是线程安全的,当多个生产者或消费者同时操作队列时需要保护。
QMutex mutex;// --- 共享资源 ---
// 共享的缓冲区,我们用一个队列来模拟
QQueue<int> buffer;// --- 生产者线程 ---
class Producer : public QThread
{
public:void run() override{for (int i = 0; i < 10; ++i){// 1. 请求一个空闲位置。// 如果 freeSlots 计数器 > 0,它会减1并立即返回。// 如果 freeSlots 计数器 == 0,此线程会阻塞,直到有消费者释放了一个位置。freeSlots.acquire();// 2. 锁住缓冲区,进行写入操作mutex.lock();int value = i * 10;buffer.enqueue(value);qDebug() << "生产者" << this->objectName() << "生产了数据:" << value << ", 当前缓冲区大小:" << buffer.size();mutex.unlock();// 3. 释放一个“已使用”位置的信号。// 这会使 usedSlots 计数器加1,并可能唤醒一个正在等待的消费者线程。usedSlots.release();msleep(50); // 生产慢一点}}
};// --- 消费者线程 ---
class Consumer : public QThread
{
public:void run() override{for (int i = 0; i < 10; ++i){// 1. 请求一个“已使用”的位置。// 如果 usedSlots 计数器 > 0,它会减1并立即返回。// 如果 usedSlots 计数器 == 0 (缓冲区是空的),此线程会阻塞,直到有生产者生产了数据。usedSlots.acquire();// 2. 锁住缓冲区,进行读取操作mutex.lock();int value = buffer.dequeue();qDebug() << "消费者" << this->objectName() << "消费了数据:" << value << ", 当前缓冲区大小:" << buffer.size();mutex.unlock();// 3. 释放一个“空闲”位置的信号。// 这会使 freeSlots 计数器加1,并可能唤醒一个正在等待的生产者线程。freeSlots.release();msleep(100); // 消费慢一点}}
};int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);qDebug() << "--- 生产者-消费者问题示例 ---";qDebug() << "缓冲区大小:" << BufferSize;// 创建并启动线程Producer producer1;producer1.setObjectName("P1");Consumer consumer1;consumer1.setObjectName("C1");producer1.start();consumer1.start();// 等待线程结束producer1.wait();consumer1.wait();qDebug() << "所有任务完成。";return 0;
}
2.4 QWaitCondition (等待条件)
用于让线程在满足特定条件之前进入休眠等待状态,避免了使用循环不断轮询检查,从而节省 CPU 资源。它总是和 QMutex
配合使用。
-
wait(QMutex *mutex)
: 原子地解锁互斥锁并使线程进入等待状态。当被唤醒时,它会重新锁上互斥锁再返回。 -
wakeOne()
: 随机唤醒一个正在等待的线程。 -
wakeAll()
: 唤醒所有正在等待的线程。
为什么要和 QMutex 配合? 为了防止在检查条件和进入等待状态之间发生条件变化(“丢失的唤醒”问题),wait()
操作必须是原子的。
#include <QCoreApplication>
#include <QThread>
#include <QWaitCondition>
#include <QMutex>
#include <QQueue>
#include <QDebug>// --- 缓冲区大小 ---
const int BufferSize = 5;// --- 同步工具 ---
QMutex mutex; // 必须与 QWaitCondition 配合使用的互斥锁
QWaitCondition bufferNotEmpty; // 条件:缓冲区不为空 (用于通知消费者)
QWaitCondition bufferNotFull; // 条件:缓冲区未满 (用于通知生产者)// --- 共享资源 ---
QQueue<int> buffer; // 共享缓冲区// --- 生产者线程 ---
class Producer : public QThread
{
public:void run() override{for (int i = 0; i < 10; ++i){mutex.lock(); // 进入临界区前加锁// 1. 检查条件:如果缓冲区已满while (buffer.size() == BufferSize) {qDebug() << "生产者" << this->objectName() << "发现缓冲区已满,进入等待...";// 原子操作:// a. 解锁 mutex// b. 线程进入休眠等待状态// c. 当被唤醒时,它会重新锁上 mutex 再继续执行bufferNotFull.wait(&mutex);}// 2. 执行操作int value = i * 10;buffer.enqueue(value);qDebug() << "生产者" << this->objectName() << "生产了数据:" << value << ", 当前缓冲区大小:" << buffer.size();// 3. 通知其他线程// 唤醒一个可能正在等待“缓冲区不为空”条件的消费者线程bufferNotEmpty.wakeOne();mutex.unlock(); // 离开临界区后解锁msleep(50);}}
};// --- 消费者线程 ---
class Consumer : public QThread
{
public:void run() override{for (int i = 0; i < 10; ++i){mutex.lock(); // 进入临界区前加锁// 1. 检查条件:如果缓冲区为空while (buffer.isEmpty()) {qDebug() << "消费者" << this->objectName() << "发现缓冲区为空,进入等待...";// 原子操作:等待“缓冲区不为空”的信号bufferNotEmpty.wait(&mutex);}// 2. 执行操作int value = buffer.dequeue();qDebug() << "消费者" << this->objectName() << "消费了数据:" << value << ", 当前缓冲区大小:" << buffer.size();// 3. 通知其他线程// 唤醒一个可能正在等待“缓冲区未满”条件的生产者线程bufferNotFull.wakeOne();mutex.unlock(); // 离开临界区后解锁msleep(100);}}
};int main(int argc, char *argv[])
{QCoreApplication a(argc, argv);qDebug() << "--- QWaitCondition 生产者-消费者示例 ---";Producer producer1;producer1.setObjectName("P1");Consumer consumer1;consumer1.setObjectName("C1");producer1.start();consumer1.start();producer1.wait();consumer1.wait();qDebug() << "所有任务完成。";return 0;
}
3. 线程间通信:信号和槽
Qt 的信号和槽机制是线程间通信的首选方式,因为它类型安全、简单易用,并且 Qt 内部已经处理好了所有复杂的同步细节。
连接类型 (Qt::ConnectionType)
-
Qt::AutoConnection
(默认):-
如果信号发射者和接收者在同一线程,则行为同
Qt::DirectConnection
。 -
如果信号发射者和接收者在不同线程,则行为同
Qt::QueuedConnection
。
-
-
Qt::DirectConnection
: 槽函数在信号发射的线程中被立即执行。跨线程使用时必须确保槽函数是线程安全的,否则极易引发问题。 -
Qt::QueuedConnection
(队列连接): 这是跨线程通信的核心。当信号发射时,Qt 会将这个事件(包含调用的槽函数和参数)放入接收者所在线程的事件队列中。当接收者线程的事件循环处理到这个事件时,才会执行对应的槽函数。这确保了槽函数总是在其所属的对象所在的线程中安全地执行。 -
Qt::BlockingQueuedConnection
: 与队列连接类似,但信号发射的线程会阻塞,直到槽函数执行完毕。这可以用于需要从另一个线程同步获取结果的场景,但要小心使用,因为它可能导致死锁。
在 moveToThread
模式中,我们正是利用了 Qt::AutoConnection
自动变为 Qt::QueuedConnection
的特性,来安全地从工作线程向主 GUI 线程发送数据,或者从主线程向工作线程发送指令。
4. QtConcurrent 框架
QtConcurrent
是一个高级 API,它构建在 QThread
和 QThreadPool
之上,旨在简化常见的并发编程模式,让你无需手动管理 QThread
对象。
4.1 QtConcurrent::run()
这是最常用的函数,它可以方便地在一个后台线程中执行一个函数。它会从 Qt 的全局线程池中取一个空闲线程来执行任务。
// 假设有一个全局函数或类的静态方法
int computeSomething(int param) {QThread::sleep(2); // 模拟耗时计算return param * param;
}void MyClass::startComputation() {// 异步执行 computeSomethingQFuture<int> future = QtConcurrent::run(computeSomething, 42);// ... 此时主线程可以继续做其他事情,不会被阻塞 ...
}
4.2 QFuture
和 QFutureWatcher
QtConcurrent::run()
会立即返回一个 QFuture
对象。QFuture
是一个占位符,代表了未来某个时刻才会知道的计算结果。
-
阻塞式获取结果: 你可以调用
future.result()
来获取结果,但这会阻塞当前线程,直到计算完成。这在主线程中是不可取的。 -
非阻塞式获取结果 (推荐): 使用
QFutureWatcher
来监视QFuture
的状态。QFutureWatcher
通过信号槽机制通知你任务的进展。
示例:
#include <QtConcurrent>
#include <QFuture>
#include <QFutureWatcher>
#include <QLabel>class MyWindow : public QWidget {Q_OBJECT
public:MyWindow() {// ...resultLabel = new QLabel("正在计算...", this);watcher = new QFutureWatcher<int>(this);// 当计算完成时,调用 onFinished 槽connect(watcher, &QFutureWatcher<int>::finished, this, &MyWindow::onFinished);// 启动计算QFuture<int> future = QtConcurrent::run(this, &MyWindow::longComputation);watcher->setFuture(future);}private:int longComputation() {qDebug() << "后台计算开始于线程:" << QThread::currentThread();QThread::sleep(3);return 123;}private slots:void onFinished() {qDebug() << "结果已返回,处理于线程:" << QThread::currentThread();int result = watcher->result();resultLabel->setText(QString("计算结果: %1").arg(result));}private:QLabel *resultLabel;QFutureWatcher<int> *watcher;
};
QtConcurrent
还提供了 map
, mapped
, filter
等函数,用于对一个容器(如 QList
)中的所有元素并行地执行某个操作,极大地简化了数据并行处理的编码。
总结
-
首选
moveToThread
: 对于需要长期运行、有复杂状态或需要与主线程频繁交互的任务,使用moveToThread
模式是最健壮和灵活的选择。 -
善用
QtConcurrent
: 对于一次性的、简单的后台任务,QtConcurrent::run
结合QFutureWatcher
是最快捷、最简单的解决方案。 -
通信靠信号槽: 始终优先使用信号和槽(特别是队列连接)进行线程间通信。
-
同步要谨慎: 只有在多个线程确实需要直接访问共享内存时,才使用
QMutex
等同步原语,并优先选择 RAII 风格的QMutexLocker
等辅助类。 -
严禁
terminate()
: 永远不要使用QThread::terminate()
来终止线程。
掌握了这些知识点,你就能在 Qt 中编写出高效、稳定且用户体验优秀的多线程应用程序。