第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 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速参考。

QT多线程 QThread 继承run() moveToThread() 线程同步 QMutex QSemaphore QReadWriteLock QRunnable QThreadPool 轻量级任务 QtConcurrent QtConcurrent::run mapped / filtered 线程安全 数据竞争 死锁预防 原子操作 选择原则:简单任务用QtConcurrent,复杂控制用QThread

19.7 避坑指南

最后,分享几个我踩过的坑,希望能帮你少走弯路。

  • 不要在子线程创建QWidget:所有界面组件必须在主线程创建和操作。我曾经在子线程new了一个QProgressDialog,程序直接崩溃。
  • QThread析构前要wait():如果线程还在跑,你直接把QThread对象删了,会出大问题。记得先调用quit()和wait()。
  • 信号槽连接类型要注意:跨线程的信号槽,默认是QueuedConnection。如果你需要同步等待结果,可以用Qt::BlockingQueuedConnection,但要小心死锁。
  • 不要滥用多线程:线程切换是有开销的。如果任务执行时间很短(几毫秒),用多线程反而更慢。我一般只在任务超过100ms时才考虑开线程。

💡 我的建议:刚开始学多线程,先从QtConcurrent::run入手。它最简单,不容易出错。等你对线程安全有了感觉,再尝试QThread和手动同步。循序渐进,别一口吃成胖子。

好了,这一章的内容就到这里。多线程是个大话题,但掌握了这些核心工具,你已经能应对大部分场景了。记住,写多线程代码时,多想想数据安全,少写点花哨的优化——稳定比快更重要。


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