第19章 QT多线程:QThread、线程同步、QMutex、QSemaphore、QRunnable、QtConcurrent与线程安全
多线程,说白了就是让程序同时干好几件事。我刚开始学QT那会儿,觉得单线程跑得挺顺,干嘛要搞这么复杂?直到有一次写一个文件批量处理工具,界面卡得跟死机一样,用户点取消都没反应……嗯,从那以后,我再也不敢忽视多线程了。
这一章,咱们就把QT多线程的几把刷子都捋一遍。我会结合自己踩过的坑,给你讲清楚什么时候该用谁,怎么用才安全。
19.1 QThread:最基础的多线程方案
QThread是QT多线程的基石。你想想看,几乎所有高级的多线程API,底层都是基于它实现的。
19.1.1 两种使用方式
第一种,继承QThread,重写run()函数。我个人习惯用这种方式,因为代码结构清晰。
class WorkerThread : public QThread
{
Q_OBJECT
protected:
void run() override
{
// 在这里执行耗时操作
for (int i = 0; i < 100; ++i) {
if (isInterruptionRequested())
return;
// 处理数据...
emit progressChanged(i);
}
}
signals:
void progressChanged(int value);
};
// 使用
WorkerThread *thread = new WorkerThread(this);
connect(thread, &WorkerThread::finished, this, &MyClass::onFinished);
thread->start();
第二种,使用moveToThread()。这种方式更灵活,可以把一个已有对象搬到子线程里跑。
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork()
{
// 耗时操作
}
};
QThread *thread = new QThread;
Worker *worker = new Worker;
worker->moveToThread(thread);
connect(thread, &QThread::started, worker, &Worker::doWork);
thread->start();
⚠️ 注意:千万不要在QThread的run()里操作界面组件。QT的UI只能在主线程里更新。我曾经犯过这个错,程序直接崩溃,查了半天才发现是线程安全问题。
19.2 线程同步:别让数据打架
多线程最头疼的问题是什么?数据竞争。两个线程同时写一个变量,结果就乱套了。我有个项目,线上跑了好几天,突然数据对不上了,查到最后就是少加了一把锁。
19.2.1 QMutex:互斥锁
QMutex是最常用的同步工具。它保证同一时间只有一个线程能访问共享资源。
QMutex mutex;
int sharedCounter = 0;
void incrementCounter()
{
mutex.lock();
sharedCounter++;
mutex.unlock();
}
// 更安全的写法:使用QMutexLocker
void safeIncrement()
{
QMutexLocker locker(&mutex);
sharedCounter++;
// 离开作用域时自动解锁
}
我建议你尽量用QMutexLocker。为什么?因为万一函数中间抛出异常或者提前return,手动lock/unlock很容易忘记解锁。QMutexLocker利用RAII机制,自动管理锁的生命周期,省心多了。
19.2.2 QSemaphore:信号量
信号量适合控制对有限资源的访问。比如,你有一个线程池,最多同时跑4个任务,用QSemaphore就很合适。
QSemaphore semaphore(4); // 最多允许4个线程同时访问
void processTask()
{
semaphore.acquire(); // 获取一个资源,如果没有则阻塞
// 执行任务...
semaphore.release(); // 释放资源
}
我记得有一次做视频处理,需要限制同时解码的帧数,用QSemaphore一把搞定。比手动管理计数器优雅多了。
19.2.3 QReadWriteLock:读写锁
如果你的场景是“读多写少”,用QMutex就有点浪费了。QReadWriteLock允许多个线程同时读,但写的时候独占。
QReadWriteLock lock;
QMap<QString, int> cache;
int readValue(const QString &key)
{
QReadLocker locker(&lock);
return cache.value(key);
}
void writeValue(const QString &key, int value)
{
QWriteLocker locker(&lock);
cache[key] = value;
}
💡 小技巧:读写锁不是万能的。如果写操作很频繁,读写锁的性能可能还不如普通互斥锁。我一般只在读操作占比超过80%时才用它。
19.3 QRunnable与QThreadPool:轻量级任务
如果你只是临时跑一个任务,不想折腾QThread的生命周期,QRunnable是个好选择。
class MyTask : public QRunnable
{
public:
void run() override
{
// 执行任务
}
};
// 提交到线程池
MyTask *task = new MyTask();
QThreadPool::globalInstance()->start(task);
注意,QRunnable没有信号槽机制。如果你需要通知主线程结果,得自己想办法。我通常会在任务里保存一个回调函数指针,或者用QMetaObject::invokeMethod间接调用。
19.4 QtConcurrent:高级并发API
QtConcurrent是QT提供的一套高级并发工具。它封装了线程池和任务调度,用起来特别爽。
19.4.1 QtConcurrent::run
最简单的用法,把函数扔到后台跑。
void heavyWork()
{
// 耗时操作
}
// 在后台线程执行
QFuture<void> future = QtConcurrent::run(heavyWork);
// 带返回值
QFuture<int> result = QtConcurrent::run([]() {
return 42;
});
19.4.2 QtConcurrent::map与filter
处理集合数据时,这些函数能自动并行化。
QList<int> data = {1, 2, 3, 4, 5};
// 并行计算平方
QFuture<int> results = QtConcurrent::mapped(data, [](int x) {
return x * x;
});
// 并行过滤
QFuture<int> filtered = QtConcurrent::filtered(data, [](int x) {
return x > 2;
});
我个人特别喜欢QtConcurrent::mapped。以前写图像处理,要对每个像素做变换,用这个函数一行代码就搞定了并行化,性能提升好几倍。
19.5 线程安全:写代码的底线
线程安全不是一种技术,而是一种意识。说白了,就是你的代码在多线程环境下不会出问题。
19.5.1 基本原则
- 不要共享可变数据:能不用全局变量就不用
- 如果必须共享,加锁:QMutex、QReadWriteLock选一个
- 尽量用信号槽通信:QT的信号槽是线程安全的,跨线程emit没问题
- 避免死锁:多个锁的获取顺序要一致
19.5.2 QT容器线程安全吗?
直接回答:不安全。QList、QMap这些容器都不是线程安全的。多线程同时读写,必须自己加锁。
QT提供了几个线程安全的类:
| 类名 | 用途 | 说明 |
|---|---|---|
| QMutex | 互斥锁 | 最常用 |
| QReadWriteLock | 读写锁 | 读多写少场景 |
| QSemaphore | 信号量 | 控制资源数量 |
| QAtomicInt | 原子整数 | 无锁操作,性能高 |
| QThreadStorage | 线程局部存储 | 每个线程独立副本 |
🔑 核心要点:多线程编程,80%的问题出在数据竞争上。剩下的20%是死锁和性能问题。写代码时多问自己一句:“这个变量会被多个线程同时访问吗?”
19.6 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速参考。
19.7 避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路。
- 不要在子线程创建QWidget:所有界面组件必须在主线程创建和操作。我曾经在子线程new了一个QProgressDialog,程序直接崩溃。
- QThread析构前要wait():如果线程还在跑,你直接把QThread对象删了,会出大问题。记得先调用quit()和wait()。
- 信号槽连接类型要注意:跨线程的信号槽,默认是QueuedConnection。如果你需要同步等待结果,可以用Qt::BlockingQueuedConnection,但要小心死锁。
- 不要滥用多线程:线程切换是有开销的。如果任务执行时间很短(几毫秒),用多线程反而更慢。我一般只在任务超过100ms时才考虑开线程。
💡 我的建议:刚开始学多线程,先从QtConcurrent::run入手。它最简单,不容易出错。等你对线程安全有了感觉,再尝试QThread和手动同步。循序渐进,别一口吃成胖子。
好了,这一章的内容就到这里。多线程是个大话题,但掌握了这些核心工具,你已经能应对大部分场景了。记住,写多线程代码时,多想想数据安全,少写点花哨的优化——稳定比快更重要。
公众号:蓝海资料掘金营,微信deep3321