24、Qt多线程编程:QThread基础、线程同步(QMutex, QSemaphore)、QThreadPool与QtConcurrent
多线程编程,说白了就是让程序同时干好几件事。你想想看,如果UI界面在加载大文件时卡住不动,用户肯定会骂娘。我刚开始用Qt做项目时,就踩过这个坑——界面假死,用户体验极差。后来才明白,多线程是解决这类问题的关键。
Qt提供了好几套多线程方案,从基础的QThread到高级的QtConcurrent,各有各的适用场景。今天我就把这些东西掰开揉碎了讲给你听。
QThread:最基础的多线程方式
QThread是Qt多线程的基石。我个人习惯把它当作一个「线程管理器」,而不是线程本身。怎么理解?看代码:
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork() {
// 耗时操作
for (int i = 0; i < 100; ++i) {
emit progress(i);
QThread::msleep(50);
}
emit finished();
}
signals:
void progress(int value);
void finished();
};
// 使用方式
QThread* thread = new QThread;
Worker* worker = new Worker;
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Worker::doWork);
connect(worker, &Worker::finished, thread, &QThread::quit);
connect(worker, &Worker::finished, worker, &Worker::deleteLater);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
嗯,这里要注意:千万别直接继承QThread然后重写run()。虽然也能用,但不够灵活。把工作对象移到线程里,才是Qt推荐的做法。我在项目中遇到过有人直接继承QThread,结果信号槽跨线程连接出了问题,排查了半天。
线程同步:QMutex与QSemaphore
多线程最头疼的问题是什么?数据竞争。两个线程同时修改同一个变量,结果就乱套了。这时候就需要同步机制。
QMutex:互斥锁
QMutex是最常用的同步工具。它保证同一时间只有一个线程能访问共享资源。
QMutex mutex;
int sharedCounter = 0;
void increment() {
mutex.lock();
sharedCounter++;
mutex.unlock();
}
// 更安全的写法:QMutexLocker
void safeIncrement() {
QMutexLocker locker(&mutex);
sharedCounter++;
// 离开作用域时自动解锁
}
我个人强烈建议用QMutexLocker,而不是手动lock/unlock。为什么?因为如果函数中间抛出异常或者提前return,你很容易忘记解锁。QMutexLocker利用RAII机制,自动管理锁的生命周期。
QSemaphore:信号量
信号量可以理解为「加强版的互斥锁」。它允许多个线程同时访问资源,但限制最大并发数。
QSemaphore semaphore(3); // 最多允许3个线程同时访问
void accessResource() {
semaphore.acquire(); // 获取一个许可
// 访问共享资源
semaphore.release(); // 释放许可
}
信号量特别适合「生产者-消费者」模式。我记得有个项目需要从网络接收数据包,然后多线程处理。用QSemaphore控制缓冲区,完美解决了生产速度和消费速度不匹配的问题。
| 同步工具 | 适用场景 | 特点 |
|---|---|---|
| QMutex | 保护共享数据 | 一次只允许一个线程访问 |
| QSemaphore | 控制并发数量 | 可设置最大并发数 |
| QReadWriteLock | 读多写少场景 | 读可并发,写独占 |
| QWaitCondition | 线程间条件等待 | 配合QMutex使用 |
QThreadPool与QtConcurrent:高级多线程
手动管理线程太累了。创建、销毁、复用……这些琐事完全可以交给框架。QThreadPool就是干这个的。
QThreadPool:线程池
线程池维护一组工作线程,避免频繁创建销毁线程的开销。默认的全局线程池已经够用:
// 使用QRunnable
class MyTask : public QRunnable
{
public:
void run() override {
qDebug() << "任务在线程中执行:" << QThread::currentThread();
}
};
// 提交任务
QThreadPool::globalInstance()->start(new MyTask());
线程池默认的线程数等于CPU核心数。你可以通过setMaxThreadCount()调整。不过我个人建议别乱改,除非你很清楚自己在做什么。我曾经为了压榨性能把线程数设成核心数的两倍,结果上下文切换开销反而拖慢了整体速度。
QtConcurrent:更高级的抽象
QtConcurrent把多线程编程简化到了极致。你甚至不需要关心线程的创建和管理:
#include <QtConcurrent>
// 异步执行函数
QFuture<void> future = QtConcurrent::run([]() {
// 耗时操作
qDebug() << "在后台线程执行";
});
// 获取返回值
QFuture<int> resultFuture = QtConcurrent::run([]() {
return 42;
});
int result = resultFuture.result(); // 阻塞等待结果
// 并行处理容器
QList<int> list = {1, 2, 3, 4, 5};
QFuture<int> mapped = QtConcurrent::mapped(list, [](int value) {
return value * 2;
});
QtConcurrent::run()会自动从全局线程池中获取线程。你想想看,以前要写几十行代码才能实现的功能,现在一行就搞定了。
- QThread适合需要精细控制线程生命周期的场景
- QThreadPool适合大量短任务的场景
- QtConcurrent适合「只管结果,不管过程」的场景
- 线程同步是必须的,但尽量用高级API减少手动加锁
最后说一句:多线程不是银弹。我见过有人为了用多线程而用多线程,结果代码复杂度暴增,性能反而下降。记住一个原则:能用单线程解决的问题,就别用多线程。只有当确实需要并行处理或者防止UI卡顿时,才考虑引入多线程。