目录
[1. QThread 的正确用法:工作者-控制器模式](#1. QThread 的正确用法:工作者-控制器模式)
[1.1 为什么不推荐继承 QThread?](#1.1 为什么不推荐继承 QThread?)
[1.2 正确模式:moveToThread()](#1.2 正确模式:moveToThread())
[1.3 安全地停止线程](#1.3 安全地停止线程)
[2. 线程同步](#2. 线程同步)
[2.1 QMutex (互斥锁)](#2.1 QMutex (互斥锁))
[2.2 QReadWriteLock (读写锁)](#2.2 QReadWriteLock (读写锁))
[2.3 QSemaphore (信号量)](#2.3 QSemaphore (信号量))
[2.4 QWaitCondition (等待条件)](#2.4 QWaitCondition (等待条件))
[3. 线程间通信:信号和槽](#3. 线程间通信:信号和槽)
[4. QtConcurrent 框架](#4. QtConcurrent 框架)
[4.1 QtConcurrent::run()](#4.1 QtConcurrent::run())
[4.2 QFuture 和 QFutureWatcher](#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
的实例,并建立它们之间的关系。
cpp
#include <QCoreApplication>
#include <QThread>
#include <QObject>
#include <QDebug>
// 推荐的方式:创建一个继承自 QObject 的 Worker 类
class Worker : public QObject
{
Q_OBJECT
public:
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_OBJECT
QThread workerThread; // QThread 对象作为成员变量
public:
Controller(QObject *parent = nullptr) : QObject(parent)
{
Worker *worker = new Worker; // 1. 创建 Worker
worker->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()
: 等待线程完全执行完毕并退出。这可以确保在继续主线程逻辑之前,子线程已经完全清理干净。
cpp
#include <QCoreApplication>
#include <QThread>
#include <QObject>
#include <QDebug>
#include <atomic> // C++11原子操作库,用于线程安全的布尔标志
// 工作者类,负责执行耗时任务
class Worker : public QObject
{
Q_OBJECT
private:
// 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) 技术。在构造时自动上锁,在析构时(离开作用域)自动解锁,从而避免了忘记解锁导致的死锁问题。
cpp
#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
。
cpp
#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 个资源,唤醒可能正在等待的线程。
一个经典的应用场景是控制生产者-消费者问题中的缓冲区大小。
cpp
#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()
操作必须是原子的。
cpp
#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() {
// 异步执行 computeSomething
QFuture<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 中编写出高效、稳定且用户体验优秀的多线程应用程序。