第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;
};
我曾经踩过的坑:忘记在const成员函数里把mutex声明为mutable。编译器报错时我还纳闷了半天,明明只是读数据啊。记住,读操作也要加锁,除非你能保证没有写线程在同时操作。

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);
我建议:不要盲目设大线程数。CPU密集型的任务,线程数等于核心数就够了。I/O密集型的可以适当多一些。设太多反而会因为上下文切换降低性能。

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提供了几个线程安全的类:

  • QMutexQReadWriteLock:锁机制
  • QAtomicIntQAtomicPointer:原子操作
  • QThreadStorage:线程局部存储

关于QObject的线程安全,有个重要原则:QObject不是线程安全的,但它的信号槽是线程安全的。什么意思呢?就是你不能在非主线程里直接操作UI控件,但可以通过信号槽把数据发回主线程。

我曾经犯过的错:在后台线程里直接调用QLabel::setText()。程序时不时崩溃,查了两天才发现是线程问题。记住,所有UI操作必须在主线程进行。用信号槽把数据传回来,或者用QMetaObject::invokeMethod。

19.6 知识体系总览

这一章内容比较多,我画了一张图帮你理清思路:

Qt多线程编程知识体系 多线程编程 QThread 线程同步 QRunnable+QThreadPool QtConcurrent moveToThread 事件循环 QMutex QReadWriteLock QSemaphore QWaitCondition 全局线程池 自定义线程池 QtConcurrent::run mapped/filtered QFutureWatcher 核心原则:UI操作必须在主线程,数据操作要加锁

这张图把Qt多线程的四个主要方向都串起来了。从上往下看,先选工具,再看具体用法。我个人建议新手先从QThread+moveToThread入手,理解线程和对象的关系。等用熟了,再尝试QtConcurrent这种更高级的抽象。

核心要点回顾:

  • QThread是管理线程的对象,不是线程本身。用moveToThread方式更优雅
  • 线程同步用QMutexLocker,别手动管理锁
  • QRunnable+QThreadPool适合大量小任务,避免频繁创建线程
  • QtConcurrent让多线程代码像函数式编程一样简洁
  • UI操作永远在主线程,数据共享永远要加锁

多线程编程说白了就是「分工协作」四个字。把任务分好,把数据保护好,剩下的交给Qt框架。你想想看,其实没那么复杂。我在实际项目中,80%的多线程场景用QtConcurrent就搞定了,剩下的用QThreadPool或者手动管理QThread。选对工具,事半功倍。


公众号:蓝海资料掘金营,微信deep3321