2. QThread基础:创建第一个QThread子类,重写run()方法

好,咱们正式开始动手写多线程代码了。

说实话,很多初学者一上来就被QThread的各种用法搞晕了。什么moveToThread、什么信号槽跨线程……其实最基础、最直观的方式,就是继承QThread,重写它的run()方法。我个人习惯先用这种方式入门,等理解了线程的生命周期,再去玩那些高级玩法。

2.1 为什么需要继承QThread?

QThread本身不是一个线程,它是线程的管理器。真正的线程执行体,是run()里面的代码。

默认的run()会调用exec()启动事件循环。但如果我们只是想做点计算任务,不需要事件循环,那就直接重写run(),把耗时操作放进去。

核心理解:

  • QThread对象活在创建它的线程里(通常是主线程)
  • run()里的代码,才真正跑在新线程里
  • 重写run()是最直接的控制方式

2.2 写一个最简单的QThread子类

来,直接上代码。我带你写一个能打印线程ID的工作线程。

// workerthread.h
#ifndef WORKERTHREAD_H
#define WORKERTHREAD_H

#include <QThread>
#include <QDebug>

class WorkerThread : public QThread
{
    Q_OBJECT

public:
    explicit WorkerThread(QObject *parent = nullptr);
    ~WorkerThread();

protected:
    void run() override;
};

#endif // WORKERTHREAD_H
// workerthread.cpp
#include "workerthread.h"

WorkerThread::WorkerThread(QObject *parent)
    : QThread(parent)
{
}

WorkerThread::~WorkerThread()
{
}

void WorkerThread::run()
{
    // 这里的代码跑在子线程中
    qDebug() << "子线程ID:" << QThread::currentThreadId();

    // 模拟耗时操作
    for (int i = 0; i < 5; ++i) {
        qDebug() << "工作中..." << i;
        QThread::sleep(1);  // 睡1秒,模拟耗时
    }

    qDebug() << "子线程工作完成";
}

嗯,这里要注意:QThread::sleep()会让当前线程阻塞,但不会阻塞主线程。这就是多线程的意义——各干各的。

2.3 在主线程中启动它

写好了子类,怎么用?很简单:创建对象,调用start()。

// main.cpp 片段
#include <QCoreApplication>
#include "workerthread.h"

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

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

    WorkerThread *worker = new WorkerThread();
    worker->start();  // 启动线程,run()开始执行

    // 主线程继续做自己的事
    for (int i = 0; i < 3; ++i) {
        qDebug() << "主线程工作中..." << i;
        QThread::sleep(1);
    }

    // 等待子线程结束
    worker->wait();

    delete worker;
    return 0;
}

运行后你会看到两个线程的ID不一样,而且打印是交替出现的。这说明它们真的在并行跑。

小技巧:我习惯在run()开头就打印线程ID,方便调试时确认代码确实跑在子线程里。曾经有一次我忘了重写run(),结果代码还在主线程跑,界面照样卡死……排查了半天才发现。

2.4 线程的生命周期管理

写多线程,最怕的就是线程还没跑完,对象就被销毁了。我见过不少崩溃都是因为这个。

方法 作用 注意事项
start() 启动线程,调用run() 不要重复调用,除非线程已结束
wait() 阻塞等待线程结束 在析构函数中调用,防止野线程
isRunning() 判断线程是否在运行 常用于条件判断
quit()/exit() 退出事件循环 如果没事件循环,调用无效

警告:千万不要在run()还没结束时就delete线程对象!这会导致程序崩溃。正确的做法是:先wait()确保线程结束,再delete。或者把线程对象设置成setAttribute(Qt::WA_DeleteOnClose),但我不推荐新手这么玩。

2.5 避坑指南:run()里别碰GUI

这一点我必须强调:所有Qt GUI操作,只能在主线程执行。

为什么?因为Qt的UI组件不是线程安全的。你在子线程里更新界面,轻则显示异常,重则直接崩溃。

我曾经在一个项目里,直接在run()里调用了ui->label->setText(),结果程序时不时闪退,查了两天才找到原因。从那以后,我养成了习惯:run()里只做计算,界面更新一律通过信号槽发回主线程。

黄金法则:

  • 子线程:做计算、读写文件、网络请求
  • 主线程:更新UI、处理用户输入
  • 通信方式:信号槽(后面章节会详细讲)

2.6 本章知识体系

下面这张图,帮你理清QThread子类方式的核心逻辑:

主线程 创建 WorkerThread 对象 调用 start() 启动线程 继续处理 UI 事件 调用 wait() 等待结束 子线程 run() 开始执行 执行耗时计算 通过信号发送结果 run() 执行完毕,线程退出 图:QThread子类方式的主线程与子线程交互流程

说白了,整个流程就是:主线程创建并启动子线程,子线程在run()里干活,干完了自动退出。主线程通过wait()确认它真的结束了,再安全地清理资源。

2.7 什么时候用这种方式?

我个人觉得,继承QThread重写run()最适合以下场景:

  • 任务是一次性的,不需要反复启动停止
  • 任务不需要事件循环(比如纯计算、文件处理)
  • 你想快速验证多线程效果,不想引入复杂机制

如果你的线程需要响应外部命令、需要反复处理多个任务,那moveToThread方式会更合适。这个我们后面会讲到。

我的建议:初学者先用继承QThread的方式跑通第一个多线程程序,感受一下线程的并行效果。等理解了线程的生命周期和信号槽通信,再去尝试其他模式。别一口吃成胖子。

好了,这一章就到这里。代码不多,但都是核心。你动手写一遍,跑起来看看打印效果,比看十遍文章都管用。


可可精品资料库