2. Qt多线程基础

QThread 是 Qt 多线程编程的核心类,它并非一个"线程对象",而是一个"线程控制句柄"。理解 QThread 的本质、生命周期和正确使用方式,是构建稳定、高效多线程应用的前提。本节将深入剖析 QThread 的创建、启动、运行与销毁全过程,并重点澄清 run() 重写的适用场景与线程优先级的实际影响。

QThread 生命周期与执行模型 主线程 (GUI) QObject 实例 事件循环 exec() 信号/槽分发 线程亲和性: 主线程 QThread 对象 创建于主线程 new start() 创建 OS 线程 工作线程 (Worker Thread) run() 默认: exec() 进入事件循环 重写 run() 直接执行任务 无事件循环 OS 调度 信号槽跨线程连接 QueuedConnection 自动队列 信号发射 finished() 线程退出 deleteLater() 安全销毁对象 优先级: QThread::Priority NormalPriority / HighPriority / TimeCriticalPriority 生命周期: 创建 → start() → 运行 → finished() → 清理

2.1 QThread 的核心原理:线程控制句柄而非线程本身

许多初学者误以为 QThread 对象就是线程本身,这是一个根本性误解。实际上,QThread 对象是运行在创建它的线程(通常是主线程)中的一个 QObject 实例,它充当了操作系统线程的"控制句柄"。当调用 start() 时,Qt 会在操作系统层面创建一个新的线程,并让该线程执行 run() 中的代码。QThread 对象本身仍然存活于主线程,负责管理线程状态、发射信号、接收控制指令。

这种设计带来的关键影响是:QThread 对象的所有成员函数(如 start()、wait()、isRunning())都应在创建它的线程中调用,而不是在工作线程中调用。工作线程中执行的代码位于 run() 内部,它无法直接访问 QThread 对象的成员(除非通过指针,但这样做存在线程安全问题)。

核心要点:QThread 对象 = 控制面板;工作线程 = 执行引擎。控制面板在主线程,执行引擎在操作系统线程。两者通过信号槽机制进行通信。

2.2 线程生命周期:从创建到销毁的完整路径

一个 QThread 的完整生命周期包含以下阶段:

阶段 触发条件 状态标志 关键信号
创建 实例化 QThread 对象 isRunning() == false 无
启动 调用 start() isRunning() == true started()
运行 run() 执行中 isRunning() == true 自定义信号
退出 run() 返回或调用 quit()/exit() isRunning() == false finished()
清理 deleteLater() 或手动 delete 对象销毁 destroyed()

生命周期管理中最常见的错误是:线程还在运行时直接 delete QThread 对象。这会导致程序崩溃或未定义行为。正确的做法是:

  1. 连接 finished() 信号到 deleteLater() 槽,实现自动清理。
  2. 或者先调用 wait() 阻塞等待线程结束,再手动 delete。
  3. 使用 QPointer<QThread> 保护指针,防止悬垂。
风险提示:如果线程正在执行阻塞操作(如无限循环、网络等待),直接调用 wait() 会导致主线程卡死。此时应优先使用 requestInterruption() 请求中断,并在 run() 中定期检查 isInterruptionRequested()。

2.3 run() 重写:两种执行模型的抉择

run() 是 QThread 的虚函数,默认实现为调用 exec() 进入事件循环。这意味着:如果不重写 run(),工作线程将运行一个事件循环,可以处理信号槽、定时器、网络事件等。如果重写 run() 并执行耗时计算,则工作线程不会进入事件循环,无法处理任何事件。

两种模型的选择依据如下:

场景 推荐模型 原因
需要持续处理信号/槽、定时器 不重写 run(),使用默认 exec() 事件循环驱动,支持异步通信
执行一次性耗时计算 重写 run(),直接执行任务代码 避免事件循环开销,任务结束即退出
需要与 GUI 线程交互 不重写 run(),使用信号槽 跨线程信号槽依赖事件循环
执行阻塞 I/O 操作 重写 run(),但需配合中断检查 阻塞操作无法响应事件,需手动控制

重写 run() 时,必须注意:run() 中的代码运行在工作线程,不能直接访问任何 GUI 组件。如果需要更新界面,必须通过信号发射数据,由主线程的槽函数处理。

class WorkerThread : public QThread {
    Q_OBJECT
protected:
    void run() override {
        // 工作线程执行
        for (int i = 0; i < 100; ++i) {
            if (isInterruptionRequested()) break;
            emit progress(i);
            QThread::msleep(10); // 模拟耗时
        }
        emit finished();
    }
signals:
    void progress(int value);
};

2.4 线程优先级:理解与谨慎使用

QThread 提供了 setPriority() 方法,允许设置线程优先级。优先级从 IdlePriority 到 TimeCriticalPriority 共 7 个级别。但必须明确:优先级只是对操作系统调度器的"建议",实际调度行为取决于操作系统实现。Linux 下 Qt 使用 POSIX 线程,优先级映射到 nice 值;Windows 下映射到线程优先级类。

设置优先级时需注意:

  • 必须在 start() 之前或线程运行中调用,但运行中调用可能不立即生效。
  • 高优先级线程可能抢占 CPU 资源,导致低优先级线程饥饿。
  • 实时优先级(TimeCriticalPriority)可能导致系统响应异常,慎用。
  • 优先级继承问题:子线程默认继承父线程优先级。
经验建议:绝大多数应用使用 NormalPriority 即可。只有在明确需要保证响应时间(如音频处理、实时控制)时才考虑提高优先级,且应配合 QThread::yieldCurrentThread() 主动让出 CPU。

2.5 案例拆解:图像处理线程的完整实现

假设我们需要开发一个图像批量处理工具,主线程负责 GUI 显示,工作线程负责图像缩放和滤镜操作。要求:支持取消操作,实时显示进度,处理完成后通知主线程。

初始方案:直接继承 QThread,在 run() 中循环处理图像列表。但发现无法响应取消请求,且进度信号发射过于频繁导致 GUI 卡顿。

优化方案:

  1. 使用 requestInterruption() 实现取消机制,在每张图像处理前检查中断标志。
  2. 进度信号每处理 5 张图像发射一次,减少 GUI 刷新频率。
  3. 使用 Qt::QueuedConnection 确保跨线程信号安全传递。
  4. 连接 finished() 到 deleteLater(),自动清理线程对象。
class ImageProcessor : public QThread {
    Q_OBJECT
public:
    ImageProcessor(const QStringList& files, QObject* parent = nullptr)
        : QThread(parent), m_files(files) {}
    
    void run() override {
        int total = m_files.size();
        for (int i = 0; i < total; ++i) {
            if (isInterruptionRequested()) {
                emit cancelled();
                return;
            }
            // 处理图像...
            if (i % 5 == 0 || i == total - 1) {
                emit progressChanged(i + 1, total);
            }
        }
        emit completed();
    }
signals:
    void progressChanged(int current, int total);
    void completed();
    void cancelled();
private:
    QStringList m_files;
};

// 主线程使用
void MainWindow::startProcessing() {
    m_processor = new ImageProcessor(m_fileList, this);
    connect(m_processor, &ImageProcessor::progressChanged,
            this, &MainWindow::updateProgressBar);
    connect(m_processor, &ImageProcessor::completed,
            this, &MainWindow::onProcessingDone);
    connect(m_processor, &ImageProcessor::cancelled,
            this, &MainWindow::onProcessingCancelled);
    connect(m_processor, &ImageProcessor::finished,
            m_processor, &QObject::deleteLater);
    m_processor->start(QThread::LowPriority); // 降低优先级避免影响 GUI
}

结果对比:优化后,GUI 响应流畅,取消操作在 100ms 内生效,进度条更新平滑。通过降低优先级,确保了主线程的 UI 操作优先响应。

复盘经验:线程设计应遵循"单一职责"原则——线程只做计算,不做 UI;通信全部通过信号槽;生命周期管理必须闭环。优先级设置要克制,优先保证系统整体稳定性。

2.6 常见误区与总结

误区一:在 run() 中直接操作 GUI 组件。这会导致崩溃或未定义行为,必须通过信号槽间接更新。
误区二:线程未结束时 delete QThread 对象。应使用 deleteLater() 或先 wait()。
误区三:认为优先级越高越好。高优先级线程可能阻塞其他关键线程,导致系统整体性能下降。
误区四:重写 run() 后忘记调用 exec(),导致信号槽无法工作。如果需要在工作线程中使用信号槽,必须保留事件循环。

总结:QThread 的正确使用可以归纳为"三要三不要":要理解 QThread 是控制句柄而非线程本身;要明确 run() 重写与默认 exec() 的适用场景;要严格管理线程生命周期。不要在 run() 中操作 GUI;不要直接删除运行中的线程;不要滥用线程优先级。掌握这些核心原则,就能构建出稳定、高效的多线程应用。