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,结果信号槽跨线程连接出了问题,排查了半天。

避坑指南:我曾经在项目里犯过一个低级错误——在子线程里直接操作UI控件。Qt要求所有UI更新必须在主线程完成。解决办法是用信号槽,或者用QMetaObject::invokeMethod。

线程同步: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机制,自动管理锁的生命周期。

小技巧:如果只是读写一个简单的int变量,可以用QAtomicInt,性能比QMutex好得多。我优化过一个高频交易的数据处理模块,把QMutex换成QAtomicInt后,吞吐量提升了30%。

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卡顿时,才考虑引入多线程。

Qt多线程编程知识体系 QThread QMutex QSemaphore QThreadPool moveToThread 信号槽跨线程 事件循环 QMutexLocker QReadWriteLock QWaitCondition acquire/release 生产者-消费者 并发控制 QRunnable 线程复用 QtConcurrent 选择建议 简单异步任务 → QtConcurrent::run() 复杂线程控制 → QThread + moveToThread 大量短任务 → QThreadPool + QRunnable
个人经验:我建议初学者先从QtConcurrent入手,它最简单。等遇到性能瓶颈或者需要精细控制时,再深入学QThread和线程池。别一开始就想着造轮子,框架已经帮你封装好了。
公众号:蓝海资料掘金营,微信deep3321