第19章 多线程编程:QThread、线程同步、QRunnable、QThreadPool、QtConcurrent、线程安全
多线程编程,说白了就是让程序同时干好几件事。我刚开始接触Qt时,觉得单线程跑得挺顺,干嘛要自找麻烦?直到有一次做一个图片批量处理工具,界面卡得跟死机一样,用户点取消都没反应。嗯,从那以后,我再也不敢忽视多线程了。
这一章我们聊聊Qt里的多线程全家桶。内容不少,但都是实战干货。我会把每个工具的使用场景、坑点、以及我个人的习惯都讲清楚。
19.1 QThread:最基础的多线程工具
QThread是Qt多线程的基石。你想想看,几乎所有多线程方案底层都离不开它。但很多人刚接触时容易犯一个错误——把QThread当成线程本身来用。
其实QThread只是一个线程管理对象。真正的线程执行体,是它内部管理的那个操作系统线程。我见过不少新手这样写:
// 错误用法
class Worker : public QThread {
protected:
void run() override {
// 干活的代码
}
};
Worker *w = new Worker;
w->start();
这种写法虽然能跑,但破坏了QThread的设计意图。我个人习惯用「工作对象+moveToThread」的方式:
// 推荐用法
class Worker : public QObject {
Q_OBJECT
public slots:
void doWork() {
// 长时间任务
emit progress(50);
emit finished();
}
signals:
void progress(int pct);
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(thread, &QThread::finished, worker, &QObject::deleteLater);
thread->start();
19.2 线程同步:别让数据打架
多线程最头疼的问题是什么?数据竞争。两个线程同时修改同一个变量,结果就乱套了。我在项目中遇到过好几次诡异的崩溃,查到最后都是同步没做好。
Qt提供了几种常用的同步机制:
| 机制 | 适用场景 | 注意事项 |
|---|---|---|
| QMutex | 保护共享数据 | 记得用QMutexLocker,别手动lock/unlock |
| QReadWriteLock | 读多写少 | 读锁可共享,写锁独占 |
| QSemaphore | 资源计数 | 生产者-消费者模型常用 |
| QWaitCondition | 线程间等待通知 | 配合QMutex使用 |
举个例子,用QMutex保护一个计数器:
class Counter {
public:
void increment() {
QMutexLocker locker(&m_mutex);
m_value++;
}
int value() const {
QMutexLocker locker(&m_mutex);
return m_value;
}
private:
mutable QMutex m_mutex;
int m_value = 0;
};
19.3 QRunnable与QThreadPool:轻量级任务
如果你有一堆小任务要并发执行,每次都创建QThread就太浪费了。线程的创建和销毁是有开销的。这时候就该QRunnable和QThreadPool上场了。
QRunnable是个抽象类,你只需要实现run()方法。然后丢给QThreadPool去执行:
class ImageTask : public QRunnable {
public:
void run() override {
// 处理一张图片
processImage(m_path);
}
private:
QString m_path;
};
// 使用
QThreadPool::globalInstance()->start(new ImageTask("photo.jpg"));
QThreadPool内部维护了一个线程池。默认最大线程数等于CPU核心数。你可以调整:
QThreadPool::globalInstance()->setMaxThreadCount(8);
19.4 QtConcurrent:更高级的抽象
QtConcurrent是Qt提供的高级多线程API。它让你不用直接操作线程,而是用函数式编程的方式描述任务。我个人非常喜欢这个模块,代码写起来特别清爽。
常用的几个函数:
- QtConcurrent::run:在后台线程运行一个函数
- QtConcurrent::map:对序列每个元素并行执行操作
- QtConcurrent::filter:并行过滤序列
- QtConcurrent::mappedReduced:map + reduce模式
看个例子,并行处理图片列表:
QList<QString> files = {"a.jpg", "b.jpg", "c.jpg"};
// 并行处理,结果收集到QList中
QFuture<QImage> future = QtConcurrent::mapped(files, [](const QString &path) {
QImage img(path);
return img.scaled(800, 600);
});
// 获取结果(会阻塞直到完成)
QList<QImage> results = future.results();
如果你不想阻塞主线程,可以用QFutureWatcher监听完成信号:
QFutureWatcher<QImage> *watcher = new QFutureWatcher<QImage>(this);
connect(watcher, &QFutureWatcher<QImage>::finished, this, [=]() {
// 处理结果
delete watcher;
});
watcher->setFuture(future);
19.5 线程安全:Qt容器与QObject
说到线程安全,很多人以为Qt的容器都是线程安全的。其实不是。QList、QMap这些容器默认不是线程安全的。多线程同时读写,照样会崩。
Qt提供了几个线程安全的类:
- QMutex、QReadWriteLock:锁机制
- QAtomicInt、QAtomicPointer:原子操作
- QThreadStorage:线程局部存储
关于QObject的线程安全,有个重要原则:QObject不是线程安全的,但它的信号槽是线程安全的。什么意思呢?就是你不能在非主线程里直接操作UI控件,但可以通过信号槽把数据发回主线程。
19.6 知识体系总览
这一章内容比较多,我画了一张图帮你理清思路:
这张图把Qt多线程的四个主要方向都串起来了。从上往下看,先选工具,再看具体用法。我个人建议新手先从QThread+moveToThread入手,理解线程和对象的关系。等用熟了,再尝试QtConcurrent这种更高级的抽象。
核心要点回顾:
- QThread是管理线程的对象,不是线程本身。用moveToThread方式更优雅
- 线程同步用QMutexLocker,别手动管理锁
- QRunnable+QThreadPool适合大量小任务,避免频繁创建线程
- QtConcurrent让多线程代码像函数式编程一样简洁
- UI操作永远在主线程,数据共享永远要加锁
多线程编程说白了就是「分工协作」四个字。把任务分好,把数据保护好,剩下的交给Qt框架。你想想看,其实没那么复杂。我在实际项目中,80%的多线程场景用QtConcurrent就搞定了,剩下的用QThreadPool或者手动管理QThread。选对工具,事半功倍。
公众号:蓝海资料掘金营,微信deep3321