Qt 多线程与并发编程详解

目录

前言

[1. QThread 的正确用法:工作者-控制器模式](#1. QThread 的正确用法:工作者-控制器模式)

[1.1 为什么不推荐继承 QThread?](#1.1 为什么不推荐继承 QThread?)

[1.2 正确模式:moveToThread()](#1.2 正确模式:moveToThread())

[1.3 安全地停止线程](#1.3 安全地停止线程)

[2. 线程同步](#2. 线程同步)

[2.1 QMutex (互斥锁)](#2.1 QMutex (互斥锁))

[2.2 QReadWriteLock (读写锁)](#2.2 QReadWriteLock (读写锁))

[2.3 QSemaphore (信号量)](#2.3 QSemaphore (信号量))

[2.4 QWaitCondition (等待条件)](#2.4 QWaitCondition (等待条件))

[3. 线程间通信:信号和槽](#3. 线程间通信:信号和槽)

连接类型 (Qt::ConnectionType)

[4. QtConcurrent 框架](#4. QtConcurrent 框架)

[4.1 QtConcurrent::run()](#4.1 QtConcurrent::run())

[4.2 QFuture 和 QFutureWatcher](#4.2 QFuture 和 QFutureWatcher)

总结


前言

在现代桌面应用程序开发中,用户界面的流畅响应至关重要。任何耗时的操作,如复杂计算、文件 I/O、网络请求等,如果直接在主线程(GUI 线程)中执行,都会导致界面冻结,严重影响用户体验。因此,掌握多线程编程是每一位 Qt 开发者的必备技能。

本文档旨在深入探讨 Qt 中多线程和并发编程的核心概念和最佳实践,帮助你构建响应流畅、稳定可靠的应用程序。

1. QThread 的正确用法:工作者-控制器模式

初学者最容易犯的错误就是继承 QThread 并重写其 run() 方法,将耗时操作放在其中。虽然这种方法可行,但它常常会导致对线程和对象所有权的误解。

核心理念QThread 的主要职责是管理一个线程的生命周期和其事件循环,而不是作为执行耗时代码的容器。

推荐的、也是最能发挥 Qt 信号槽机制优势的模式,是将耗时任务封装在一个 QObject 子类(我们称之为"工作者",Worker)中,然后将这个工作者对象"移动"到一个新的 QThread 实例所管理的线程中去执行。

1.1 为什么不推荐继承 QThread?

当你继承 QThread 并添加了自定义的槽函数时,这些槽函数以及对象本身实际上仍然属于创建该 QThread 对象的线程 (通常是主线程),而不是 run() 方法执行所在的新线程。这意味着在 run() 之外调用这些槽函数,它们并不会在新线程中执行,这违背了多线程的初衷,并可能引发线程安全问题。

1.2 正确模式:moveToThread()

这种模式清晰地分离了线程管理和任务执行:

  • QThread (控制器): 负责启动、管理和销毁线程,并为线程提供一个事件循环。

  • QObject (工作者): 包含所有耗时任务的逻辑和数据,它的槽函数将在新线程中被执行。

实现步骤:

  1. 创建工作者类 : 创建一个继承自 QObject 的类,将耗时操作封装成一个或多个公开的槽函数。

  2. 在主线程中设置和启动线程 : 在主线程(例如 MainWindow)中,创建 QThreadWorker 的实例,并建立它们之间的关系。

cpp 复制代码
#include <QCoreApplication>
#include <QThread>
#include <QObject>
#include <QDebug>

// 推荐的方式:创建一个继承自 QObject 的 Worker 类
class Worker : public QObject
{
    Q_OBJECT

public:
    Worker(QObject *parent = nullptr) : QObject(parent)
    {
        qDebug() << "Worker 构造函数所在的线程:" << QThread::currentThreadId();
    }

    ~Worker()
    {
        qDebug() << "Worker 析构函数所在的线程:" << QThread::currentThreadId();
    }

public slots:
    // 所有的耗时操作都放在这里
    void doLongRunningTask()
    {
        qDebug() << ">>>>>> Worker::doLongRunningTask() 所在的线程:" << QThread::currentThreadId() << ">>>>>>";
        qDebug() << "进入工作函数,开始执行耗时操作...";

        for (int i = 1; i <= 3; ++i) {
            qDebug() << "正在工作..." << i;
            QThread::sleep(1);
        }

        qDebug() << "工作完成!";
        emit workFinished(); // 发出完成信号
    }

signals:
    void workFinished(); // 定义一个完成信号
};

// 这是我们的控制器类,它负责管理线程和 Worker
class Controller : public QObject
{
    Q_OBJECT
    QThread workerThread; // QThread 对象作为成员变量

public:
    Controller(QObject *parent = nullptr) : QObject(parent)
    {
        Worker *worker = new Worker; // 1. 创建 Worker
        worker->moveToThread(&workerThread); // 2. 将 Worker 移动到新线程

        // 当线程启动时,触发 Worker 的工作函数
        connect(&workerThread, &QThread::started, worker, &Worker::doLongRunningTask);

        // 当 Worker 完成工作后,安全地退出线程
        connect(worker, &Worker::workFinished, &workerThread, &QThread::quit);

        // 当线程结束后,删除 Worker 对象
        connect(&workerThread, &QThread::finished, worker, &Worker::deleteLater);

        // 当线程结束后,也可以把 Controller 也删掉(如果需要)
        // connect(&workerThread, &QThread::finished, this, &Controller::deleteLater);

        qDebug() << "准备启动线程...";
        workerThread.start(); // 3. 启动线程
    }

    ~Controller()
    {
        qDebug() << "Controller 析构";
        // 确保线程在 Controller 析构前已经停止
        workerThread.quit();
        workerThread.wait();
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    qDebug() << "主线程 ID:" << QThread::currentThreadId();

    // 创建控制器,它会自动设置并启动一切
    Controller controller;

    // 当线程结束后,Qt的事件循环会自动处理对象的删除,然后程序可以退出
    // 为了在控制台程序中看到完整流程,我们连接线程的 finished 信号到程序的 quit
    // QObject::connect(&controller.workerThread, &QThread::finished, &a, &QCoreApplication::quit);
    // 上面这句会报错,因为 workerThread 是 private 的,这里只是为了说明
    // 在GUI程序中,你不需要手动退出,事件循环会一直运行

    return a.exec();
}


#include "CorrectExample.moc"
```
**运行上述代码,您会得到类似下面的输出:**
```
主线程 ID: 0x...
Worker 构造函数所在的线程: 0x...
准备启动线程...
>>>>>> Worker::doLongRunningTask() 所在的线程: 0x...  <-- 看这里!这是一个新的线程ID
进入工作函数,开始执行耗时操作...
正在工作... 1
正在工作... 2
正在工作... 3
工作完成!
Worker 析构函数所在的线程: 0x...  <-- 在新线程中析构,非常安全
```
**结论非常清晰**:通过 `moveToThread`,我们成功地让 `Worker` 对象的所有逻辑(构造函数除外)都在一个新的、由 `QThread` 对象管理的线程中执行了。这才是 `QThread` 设计的初衷和最强大的用法。

希望这两个例子和解释能够帮助您彻底理解 `QThread` 的正确使用方式!

1.3 安全地停止线程

强制终止线程 (QThread::terminate()) 是非常危险的,它会立即结束线程,但不会执行任何清理代码(如释放内存、解锁互斥锁等),极易导致资源泄漏和死锁。

正确的停止方式

  1. 设置一个标志位 : 在工作者对象中设置一个 volatile bool 类型的标志位,例如 m_abort.

  2. 在耗时任务中检查标志 : 在循环或关键节点检查此标志位,如果为 true 则提前退出任务。

  3. 请求退出 : 主线程通过调用一个槽函数来设置这个标志位为 true

  4. 调用 quit()exit() : 这会请求线程的事件循环停止。如果线程正在执行耗时代码而没有返回事件循环,quit() 不会立即生效。

  5. 调用 wait(): 等待线程完全执行完毕并退出。这可以确保在继续主线程逻辑之前,子线程已经完全清理干净。

cpp 复制代码
#include <QCoreApplication>
#include <QThread>
#include <QObject>
#include <QDebug>
#include <atomic> // C++11原子操作库,用于线程安全的布尔标志

// 工作者类,负责执行耗时任务
class Worker : public QObject
{
    Q_OBJECT

private:
    // 1. 设置一个原子布尔类型的标志位。
    // std::atomic 可以保证多线程对它的读写操作是安全的,不会出现数据竞争。
    // 初始化为 false,表示不停止。
    std::atomic<bool> m_shouldStop{false};

public:
    Worker(QObject *parent = nullptr) : QObject(parent) {}

public slots:
    // 耗时任务的执行函数
    void doWork()
    {
        qDebug() << "工作者开始工作于线程:" << QThread::currentThreadId();
        int count = 0;

        // 2. 在耗时任务中(通常是循环)持续检查标志位
        while (!m_shouldStop)
        {
            qDebug() << "工作中..." << count++;
            // 模拟耗时操作,例如读写文件、网络通信等
            QThread::msleep(500); // 休眠500毫秒
        }

        // 当循环结束后,意味着线程收到了停止请求并完成了任务
        qDebug() << "收到停止请求,工作循环结束。";
        emit workFinished(); // 发出工作完成信号
    }

    // 3. 提供一个公共的槽函数,用于从外部线程(如主线程)设置停止标志位
    void requestStop()
    {
        qDebug() << "主线程请求停止工作...";
        m_shouldStop = true;
    }

signals:
    void workFinished(); // 工作完成信号
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    qDebug() << "主线程 ID:" << QThread::currentThreadId();

    QThread* thread = new QThread;
    Worker* worker = new Worker;

    worker->moveToThread(thread);

    // 当线程启动后,自动开始执行 doWork()
    QObject::connect(thread, &QThread::started, worker, &Worker::doWork);

    // 当工作者发出 workFinished 信号时,我们请求线程的事件循环退出
    // 4. 调用 quit() 来停止事件循环
    QObject::connect(worker, &Worker::workFinished, thread, &QThread::quit);

    // 当线程最终结束后,释放 worker 和 thread 的内存
    QObject::connect(thread, &QThread::finished, worker, &Worker::deleteLater);
    QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
    QObject::connect(thread, &QThread::finished, [&](){
        qDebug() << "线程已安全结束。";
    });

    // 启动线程
    thread->start();

    // 创建一个定时器,在3秒后从主线程调用 requestStop() 来请求停止
    QTimer::singleShot(3000, [=](){
        // 这段代码在主线程中执行
        worker->requestStop();
    });

    // a.exec() 会启动主线程的事件循环,使得信号槽和定时器可以工作
    return a.exec();
}

#include "SafeStopExample.moc"

2. 线程同步

当多个线程需要访问和修改同一个共享数据时,如果没有适当的保护,就会发生"竞态条件"(Race Condition),导致数据损坏或程序崩溃。Qt 提供了多种同步原语来解决这个问题。

2.1 QMutex (互斥锁)

QMutex 是最基本的同步工具,用于保护一个"临界区"(Critical Section),确保在任何时刻只有一个线程可以执行这段代码。

  • lock(): 获取锁。如果锁已被其他线程持有,则当前线程会阻塞,直到锁被释放。

  • unlock(): 释放锁。

  • tryLock(): 尝试获取锁,如果失败则立即返回 false,不会阻塞。

最佳实践 : 使用 QMutexLocker,它利用了 C++ 的 RAII (Resource Acquisition Is Initialization) 技术。在构造时自动上锁,在析构时(离开作用域)自动解锁,从而避免了忘记解锁导致的死锁问题。

cpp 复制代码
#include <QCoreApplication>
#include <QThread>
#include <QMutex>
#include <QDebug>
#include <QList>

// --- 共享数据 ---
// 创建一个互斥锁实例,它将保护下面的共享计数器
QMutex mutex;
int sharedCounter = 0; // 这是所有线程都想访问和修改的共享资源

// --- 线程任务 ---
// 这是一个自定义的线程类,它的任务是多次增加计数器
class WorkerThread : public QThread
{
public:
    // 线程启动后会自动执行 run() 函数
    void run() override
    {
        for (int i = 0; i < 5; ++i)
        {
            // 这是关键部分:使用 QMutexLocker 来自动管理锁
            // 当代码执行到这一行时,QMutexLocker 的构造函数会自动调用 mutex.lock() 来获取锁。
            // 如果锁被其他线程占用,这个线程会在这里等待(阻塞)。
            QMutexLocker locker(&mutex);

            // ----- 临界区开始 -----
            // 在这里面的代码是受保护的,同一时间只有一个线程可以执行。
            sharedCounter++;
            qDebug() << this->objectName() << "将计数器增加到:" << sharedCounter;
            // ----- 临界区结束 -----

            // 当 locker 对象离开这个作用域(即 for 循环的一次迭代结束)时,
            // 它的析构函数会自动被调用,从而执行 mutex.unlock() 释放锁。
            // 这就是 RAII 技术,非常安全,可以避免忘记解锁。

            // 让线程稍微休眠一下,以便观察线程间的切换
            msleep(10);
        }
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    qDebug() << "程序开始,初始计数器:" << sharedCounter;

    // 创建两个工作线程
    WorkerThread thread1;
    thread1.setObjectName("线程 A");

    WorkerThread thread2;
    thread2.setObjectName("线程 B");

    // 启动线程
    thread1.start();
    thread2.start();

    // 等待两个线程都执行完毕
    thread1.wait();
    thread2.wait();

    qDebug() << "所有线程执行完毕,最终计数器:" << sharedCounter;
    qDebug() << "期望的结果是 10 (每个线程增加5次)。如果没有锁,结果可能会小于10。";

    // a.exec(); // 对于控制台程序,可以不需要事件循环
    return 0;
}

2.2 QReadWriteLock (读写锁)

适用于"读多写少"的场景。它允许多个线程同时进行读操作,但写操作是互斥的。这比 QMutex 在读取频繁时效率更高。

  • lockForRead(): 获取读锁。

  • lockForWrite(): 获取写锁。写锁会阻塞所有其他读者和写者。

  • 最佳实践 : 相应地使用 QReadLockerQWriteLocker

cpp 复制代码
#include <QCoreApplication>
#include <QThread>
#include <QReadWriteLock>
#include <QReadLocker>
#include <QWriteLocker>
#include <QDebug>
#include <QList>

// --- 共享数据 ---
// 创建一个读写锁实例
QReadWriteLock rwLock;
// 假设这是一个共享的配置或数据,有很多线程会读取它,偶尔有线程会修改它
QString sharedMessage = "初始消息";

// --- 读线程任务 ---
class ReaderThread : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < 3; ++i)
        {
            // 使用 QReadLocker 获取读锁。
            // 多个读线程可以同时获取读锁,它们不会互相阻塞。
            QReadLocker locker(&rwLock);

            // ----- 读操作临界区 -----
            qDebug() << this->objectName() << "正在读取数据:" << sharedMessage;
            // ----- 临界区结束 -----

            // locker 离开作用域时自动释放读锁。
            msleep(15); // 休眠以观察效果
        }
    }
};

// --- 写线程任务 ---
class WriterThread : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < 2; ++i)
        {
            // 使用 QWriteLocker 获取写锁。
            // 当一个线程想要获取写锁时,它必须等待所有的读锁和写锁都被释放。
            // 一旦写锁被获取,其他任何线程(无论是读还是写)都必须等待。
            QWriteLocker locker(&rwLock);

            // ----- 写操作临界区 -----
            sharedMessage = QString("%1 写入新消息").arg(this->objectName());
            qDebug() << this->objectName() << "成功写入数据!";
            // ----- 临界区结束 -----

            // locker 离开作用域时自动释放写锁。
            msleep(25); // 休眠以观察效果
        }
    }
};


int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    qDebug() << "--- 读写锁示例 ---";

    const int readerCount = 3;
    QList<ReaderThread*> readers;
    for(int i = 0; i < readerCount; ++i) {
        readers.append(new ReaderThread());
        readers.last()->setObjectName(QString("读线程 %1").arg(i + 1));
    }

    WriterThread writer1;
    writer1.setObjectName("写线程 A");

    // 启动所有线程
    for(ReaderThread* reader : readers) {
        reader->start();
    }
    writer1.start();


    // 等待所有线程执行完毕
    for(ReaderThread* reader : readers) {
        reader->wait();
        delete reader; // 释放内存
    }
    writer1.wait();


    qDebug() << "所有线程执行完毕。";
    qDebug() << "最终消息内容:" << sharedMessage;

    return 0;
}

2.3 QSemaphore (信号量)

信号量用于保护一定数量的相同资源。它可以看作是一个"广义的互斥锁"。一个初始化为 1 的信号量等价于一个互斥锁。

  • acquire(n): 获取 n 个资源。如果资源不足,线程会阻塞。

  • release(n): 释放 n 个资源,唤醒可能正在等待的线程。

一个经典的应用场景是控制生产者-消费者问题中的缓冲区大小。

cpp 复制代码
#include <QCoreApplication>
#include <QThread>
#include <QSemaphore>
#include <QMutex>
#include <QQueue>
#include <QDebug>

// --- 缓冲区大小 ---
const int BufferSize = 5; // 我们的缓冲区最多只能存放5个整数

// --- 同步工具 ---
// 1. 信号量 freeSlots: 记录缓冲区中"空闲"位置的数量。
//    生产者在生产前需要获取一个空闲位置。初始时,所有位置都是空闲的。
QSemaphore freeSlots(BufferSize);

// 2. 信号量 usedSlots: 记录缓冲区中"已使用"位置的数量。
//    消费者在消费前需要确保有已使用的位置。初始时,没有位置被使用。
QSemaphore usedSlots(0);

// 3. 互斥锁: 保护对缓冲区队列本身的读写操作。
//    因为 QQueue 本身不是线程安全的,当多个生产者或消费者同时操作队列时需要保护。
QMutex mutex;

// --- 共享资源 ---
// 共享的缓冲区,我们用一个队列来模拟
QQueue<int> buffer;


// --- 生产者线程 ---
class Producer : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < 10; ++i)
        {
            // 1. 请求一个空闲位置。
            //    如果 freeSlots 计数器 > 0,它会减1并立即返回。
            //    如果 freeSlots 计数器 == 0,此线程会阻塞,直到有消费者释放了一个位置。
            freeSlots.acquire();

            // 2. 锁住缓冲区,进行写入操作
            mutex.lock();
            int value = i * 10;
            buffer.enqueue(value);
            qDebug() << "生产者" << this->objectName() << "生产了数据:" << value << ", 当前缓冲区大小:" << buffer.size();
            mutex.unlock();

            // 3. 释放一个"已使用"位置的信号。
            //    这会使 usedSlots 计数器加1,并可能唤醒一个正在等待的消费者线程。
            usedSlots.release();

            msleep(50); // 生产慢一点
        }
    }
};

// --- 消费者线程 ---
class Consumer : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < 10; ++i)
        {
            // 1. 请求一个"已使用"的位置。
            //    如果 usedSlots 计数器 > 0,它会减1并立即返回。
            //    如果 usedSlots 计数器 == 0 (缓冲区是空的),此线程会阻塞,直到有生产者生产了数据。
            usedSlots.acquire();

            // 2. 锁住缓冲区,进行读取操作
            mutex.lock();
            int value = buffer.dequeue();
            qDebug() << "消费者" << this->objectName() << "消费了数据:" << value << ", 当前缓冲区大小:" << buffer.size();
            mutex.unlock();

            // 3. 释放一个"空闲"位置的信号。
            //    这会使 freeSlots 计数器加1,并可能唤醒一个正在等待的生产者线程。
            freeSlots.release();

            msleep(100); // 消费慢一点
        }
    }
};


int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    qDebug() << "--- 生产者-消费者问题示例 ---";
    qDebug() << "缓冲区大小:" << BufferSize;

    // 创建并启动线程
    Producer producer1;
    producer1.setObjectName("P1");
    Consumer consumer1;
    consumer1.setObjectName("C1");

    producer1.start();
    consumer1.start();

    // 等待线程结束
    producer1.wait();
    consumer1.wait();

    qDebug() << "所有任务完成。";

    return 0;
}

2.4 QWaitCondition (等待条件)

用于让线程在满足特定条件之前进入休眠等待状态,避免了使用循环不断轮询检查,从而节省 CPU 资源。它总是和 QMutex 配合使用。

  • wait(QMutex *mutex): 原子地解锁互斥锁并使线程进入等待状态。当被唤醒时,它会重新锁上互斥锁再返回。

  • wakeOne(): 随机唤醒一个正在等待的线程。

  • wakeAll(): 唤醒所有正在等待的线程。

为什么要和 QMutex 配合? 为了防止在检查条件和进入等待状态之间发生条件变化("丢失的唤醒"问题),wait() 操作必须是原子的。

cpp 复制代码
#include <QCoreApplication>
#include <QThread>
#include <QWaitCondition>
#include <QMutex>
#include <QQueue>
#include <QDebug>

// --- 缓冲区大小 ---
const int BufferSize = 5;

// --- 同步工具 ---
QMutex mutex; // 必须与 QWaitCondition 配合使用的互斥锁
QWaitCondition bufferNotEmpty; // 条件:缓冲区不为空 (用于通知消费者)
QWaitCondition bufferNotFull;  // 条件:缓冲区未满 (用于通知生产者)

// --- 共享资源 ---
QQueue<int> buffer; // 共享缓冲区

// --- 生产者线程 ---
class Producer : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < 10; ++i)
        {
            mutex.lock(); // 进入临界区前加锁

            // 1. 检查条件:如果缓冲区已满
            while (buffer.size() == BufferSize) {
                qDebug() << "生产者" << this->objectName() << "发现缓冲区已满,进入等待...";
                // 原子操作:
                // a. 解锁 mutex
                // b. 线程进入休眠等待状态
                // c. 当被唤醒时,它会重新锁上 mutex 再继续执行
                bufferNotFull.wait(&mutex);
            }

            // 2. 执行操作
            int value = i * 10;
            buffer.enqueue(value);
            qDebug() << "生产者" << this->objectName() << "生产了数据:" << value << ", 当前缓冲区大小:" << buffer.size();

            // 3. 通知其他线程
            // 唤醒一个可能正在等待"缓冲区不为空"条件的消费者线程
            bufferNotEmpty.wakeOne();

            mutex.unlock(); // 离开临界区后解锁
            msleep(50);
        }
    }
};

// --- 消费者线程 ---
class Consumer : public QThread
{
public:
    void run() override
    {
        for (int i = 0; i < 10; ++i)
        {
            mutex.lock(); // 进入临界区前加锁

            // 1. 检查条件:如果缓冲区为空
            while (buffer.isEmpty()) {
                qDebug() << "消费者" << this->objectName() << "发现缓冲区为空,进入等待...";
                // 原子操作:等待"缓冲区不为空"的信号
                bufferNotEmpty.wait(&mutex);
            }

            // 2. 执行操作
            int value = buffer.dequeue();
            qDebug() << "消费者" << this->objectName() << "消费了数据:" << value << ", 当前缓冲区大小:" << buffer.size();

            // 3. 通知其他线程
            // 唤醒一个可能正在等待"缓冲区未满"条件的生产者线程
            bufferNotFull.wakeOne();

            mutex.unlock(); // 离开临界区后解锁
            msleep(100);
        }
    }
};


int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    qDebug() << "--- QWaitCondition 生产者-消费者示例 ---";

    Producer producer1;
    producer1.setObjectName("P1");
    Consumer consumer1;
    consumer1.setObjectName("C1");

    producer1.start();
    consumer1.start();

    producer1.wait();
    consumer1.wait();

    qDebug() << "所有任务完成。";

    return 0;
}

3. 线程间通信:信号和槽

Qt 的信号和槽机制是线程间通信的首选方式,因为它类型安全、简单易用,并且 Qt 内部已经处理好了所有复杂的同步细节。

连接类型 (Qt::ConnectionType)

  • Qt::AutoConnection (默认):

    • 如果信号发射者和接收者在同一线程 ,则行为同 Qt::DirectConnection

    • 如果信号发射者和接收者在不同线程 ,则行为同 Qt::QueuedConnection

  • Qt::DirectConnection : 槽函数在信号发射的线程中被立即执行。跨线程使用时必须确保槽函数是线程安全的,否则极易引发问题。

  • Qt::QueuedConnection (队列连接) : 这是跨线程通信的核心 。当信号发射时,Qt 会将这个事件(包含调用的槽函数和参数)放入接收者所在线程的事件队列中。当接收者线程的事件循环处理到这个事件时,才会执行对应的槽函数。这确保了槽函数总是在其所属的对象所在的线程中安全地执行。

  • Qt::BlockingQueuedConnection : 与队列连接类似,但信号发射的线程会阻塞,直到槽函数执行完毕。这可以用于需要从另一个线程同步获取结果的场景,但要小心使用,因为它可能导致死锁。

moveToThread 模式中,我们正是利用了 Qt::AutoConnection 自动变为 Qt::QueuedConnection 的特性,来安全地从工作线程向主 GUI 线程发送数据,或者从主线程向工作线程发送指令。

4. QtConcurrent 框架

QtConcurrent 是一个高级 API,它构建在 QThreadQThreadPool 之上,旨在简化常见的并发编程模式,让你无需手动管理 QThread 对象。

4.1 QtConcurrent::run()

这是最常用的函数,它可以方便地在一个后台线程中执行一个函数。它会从 Qt 的全局线程池中取一个空闲线程来执行任务。

复制代码
// 假设有一个全局函数或类的静态方法
int computeSomething(int param) {
    QThread::sleep(2); // 模拟耗时计算
    return param * param;
}

void MyClass::startComputation() {
    // 异步执行 computeSomething
    QFuture<int> future = QtConcurrent::run(computeSomething, 42);

    // ... 此时主线程可以继续做其他事情,不会被阻塞 ...
}

4.2 QFutureQFutureWatcher

QtConcurrent::run() 会立即返回一个 QFuture 对象。QFuture 是一个占位符,代表了未来某个时刻才会知道的计算结果。

  • 阻塞式获取结果 : 你可以调用 future.result() 来获取结果,但这会阻塞当前线程,直到计算完成。这在主线程中是不可取的。

  • 非阻塞式获取结果 (推荐) : 使用 QFutureWatcher 来监视 QFuture 的状态。QFutureWatcher 通过信号槽机制通知你任务的进展。

示例:

复制代码
#include <QtConcurrent>
#include <QFuture>
#include <QFutureWatcher>
#include <QLabel>

class MyWindow : public QWidget {
    Q_OBJECT
public:
    MyWindow() {
        // ...
        resultLabel = new QLabel("正在计算...", this);
        watcher = new QFutureWatcher<int>(this);

        // 当计算完成时,调用 onFinished 槽
        connect(watcher, &QFutureWatcher<int>::finished, this, &MyWindow::onFinished);

        // 启动计算
        QFuture<int> future = QtConcurrent::run(this, &MyWindow::longComputation);
        watcher->setFuture(future);
    }

private:
    int longComputation() {
        qDebug() << "后台计算开始于线程:" << QThread::currentThread();
        QThread::sleep(3);
        return 123;
    }

private slots:
    void onFinished() {
        qDebug() << "结果已返回,处理于线程:" << QThread::currentThread();
        int result = watcher->result();
        resultLabel->setText(QString("计算结果: %1").arg(result));
    }

private:
    QLabel *resultLabel;
    QFutureWatcher<int> *watcher;
};

QtConcurrent 还提供了 map, mapped, filter 等函数,用于对一个容器(如 QList)中的所有元素并行地执行某个操作,极大地简化了数据并行处理的编码。

总结

  • 首选 moveToThread : 对于需要长期运行、有复杂状态或需要与主线程频繁交互的任务,使用 moveToThread 模式是最健壮和灵活的选择。

  • 善用 QtConcurrent : 对于一次性的、简单的后台任务,QtConcurrent::run 结合 QFutureWatcher 是最快捷、最简单的解决方案。

  • 通信靠信号槽: 始终优先使用信号和槽(特别是队列连接)进行线程间通信。

  • 同步要谨慎 : 只有在多个线程确实需要直接访问共享内存时,才使用 QMutex 等同步原语,并优先选择 RAII 风格的 QMutexLocker 等辅助类。

  • 严禁 terminate() : 永远不要使用 QThread::terminate() 来终止线程。

掌握了这些知识点,你就能在 Qt 中编写出高效、稳定且用户体验优秀的多线程应用程序。

相关推荐
DrugOne3 小时前
Amber24 安装指南:Ubuntu 22.04 + CUDA 12.4 环境
linux·运维·ubuntu·drugone
消失的旧时光-19433 小时前
Kotlin Flow 与“天然背压”(完整示例)
android·开发语言·kotlin
ClassOps3 小时前
Kotlin invoke 函数调用重载
android·开发语言·kotlin
至善迎风3 小时前
Ubuntu 24.04 SSH 多端口监听与 ssh.socket 配置详解
linux·ubuntu·ssh
wdfk_prog4 小时前
[Linux]学习笔记系列 -- lib/timerqueue.c Timer Queue Management 高精度定时器的有序数据结构
linux·c语言·数据结构·笔记·单片机·学习·安全
小苏兮4 小时前
【C++】stack与queue的使用与模拟实现
开发语言·c++
大聪明-PLUS4 小时前
如何从 USB 闪存驱动器安装 Debian Linux
linux·嵌入式·arm·smarc
高山有多高4 小时前
栈:“后进先出” 的艺术,撑起程序世界的底层骨架
c语言·开发语言·数据结构·c++·算法
蔗理苦4 小时前
2025-10-07 Python不基础 19——私有对象
开发语言·python·私有对象