2. 创建第一个QThread

好,咱们正式开始动手写第一个多线程程序。

说实话,我刚开始学Qt多线程的时候,也踩过不少坑。最典型的一个——我以为重写了run()就完事了,结果界面卡死,程序崩溃,调试器里一堆看不懂的堆栈信息。后来才明白,线程这东西,不是「开个新函数跑一跑」那么简单。

这一节,咱们就老老实实从最经典的继承QThread方式开始。你把它学会了,后面再学别的线程用法,会轻松很多。

2.1 为什么选择继承QThread?

Qt提供了好几种多线程方案。但继承QThread重写run(),是最直观、最容易理解的一种。

说白了,QThread本身就是一个线程控制类。你继承它,重写里面的run()方法,然后调用start()——Qt就会在新线程里执行你写的run()里的代码。

我个人的习惯是:如果这个线程要做的事情比较独立、逻辑清晰,比如后台计算、文件处理、网络请求,我就用继承QThread的方式。代码结构清楚,维护起来也方便。

核心要点:

  • 继承QThread → 重写run() → 调用start() → 线程启动
  • run()里的代码,才是真正在新线程里执行的代码
  • 不要直接调用run(),要调用start()

2.2 写一个最简单的线程类

咱们直接上代码。我写一个WorkThread,让它在新线程里打印一些信息。

// workthread.h
#ifndef WORKTHREAD_H
#define WORKTHREAD_H

#include <QThread>
#include <QDebug>

class WorkThread : public QThread
{
    Q_OBJECT

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

protected:
    void run() override;
};

#endif // WORKTHREAD_H
// workthread.cpp
#include "workthread.h"

WorkThread::WorkThread(QObject *parent)
    : QThread(parent)
{
    qDebug() << "WorkThread 构造函数,当前线程ID:" << QThread::currentThreadId();
}

WorkThread::~WorkThread()
{
    qDebug() << "WorkThread 析构函数";
}

void WorkThread::run()
{
    qDebug() << "run() 开始执行,当前线程ID:" << QThread::currentThreadId();

    // 模拟耗时操作
    for (int i = 0; i < 5; ++i) {
        qDebug() << "正在处理第" << i+1 << "项任务...";
        QThread::sleep(1);  // 休眠1秒,模拟耗时
    }

    qDebug() << "run() 执行完毕";
}

嗯,这里要注意:QThread::sleep()是静态方法,会让当前线程(也就是新线程)休眠。千万别在主线程里调用它,否则界面会卡死。

2.3 启动线程:start() vs run()

很多新手会犯一个错误——直接调用run()。你想想看,如果直接调run(),那它就是在当前线程(主线程)里执行的,跟普通函数没区别。多线程的意义就没了。

必须调用start()。Qt会在内部创建一个新线程,然后在新线程里自动调用run()。

// main.cpp 示例
#include <QCoreApplication>
#include "workthread.h"

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

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

    WorkThread *thread = new WorkThread();
    thread->start();  // 启动线程,这才是正确姿势

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

    delete thread;
    return 0;
}

小技巧:我建议你在run()和构造函数里都打印一下QThread::currentThreadId()。这样你能亲眼看到,构造函数是在主线程执行的,而run()是在新线程执行的。这个习惯帮我排查过很多线程相关的bug。

2.4 线程的生命周期管理

线程不是启动就完事了。它有自己的生命周期:创建 → 就绪 → 运行 → 阻塞 → 终止。

我画了一张图,帮你理清这个流程:

QThread 生命周期流程图 创建 (new QThread) 就绪 (start()调用) 运行 (run()执行中) 终止 (run()返回) 关键说明 • 创建后线程未启动 • start()让线程进入就绪 • 系统调度后进入运行 • run()返回后线程终止 • 可调用wait()等待结束 • 终止后不可再次start() • 需重新new QThread • 析构前确保线程已结束 • 用isRunning()检查状态

这张图里,我特别想强调几个点:

  • 创建阶段:new WorkThread()只是创建了一个线程对象,此时还没有新线程产生。线程对象本身在主线程里。
  • 就绪阶段:调用start()后,Qt会创建真正的操作系统线程,并把它放入就绪队列。等待CPU调度。
  • 运行阶段:系统调度到该线程后,run()开始执行。这就是你写的业务逻辑。
  • 终止阶段:run()执行完毕,线程自动退出。注意,线程对象还在,但线程已经结束了。

警告:我曾经犯过一个错误——线程结束后,直接再次调用start()。结果程序崩溃了。记住:一个QThread对象只能启动一次。如果想再次运行,必须重新new一个对象。

2.5 线程的等待与安全退出

主线程退出时,如果子线程还在跑,会发生什么?

答案是:程序可能会崩溃,或者资源泄漏。因为主线程销毁了,但子线程还在访问已经被释放的内存。

正确的做法是:在析构函数或主线程退出前,调用wait()等待子线程结束。

// 安全退出示例
WorkThread *thread = new WorkThread();
thread->start();

// ... 做一些其他事情 ...

// 等待线程结束,最多等5秒
if (thread->wait(5000)) {
    qDebug() << "线程正常结束";
} else {
    qDebug() << "线程超时,强制终止";
    thread->terminate();  // 不推荐,但有时不得已
    thread->wait();       // 等待terminate生效
}

delete thread;

避坑指南:我个人强烈建议不要使用terminate()。它就像强行拔掉电源,线程可能在任意位置被中断,导致数据损坏、死锁等问题。更好的做法是在run()里设置一个标志位,通过标志位让线程自己优雅退出。这个技巧咱们后面章节会详细讲。

2.6 检查线程状态

Qt提供了几个方法,帮你随时了解线程的状态:

方法 返回值 说明
isRunning() bool 线程是否正在运行(从start()到run()返回)
isFinished() bool 线程是否已经结束
wait(unsigned long time) bool 阻塞等待线程结束,超时返回false

我写代码时,经常在调试阶段用isRunning()来确认线程是否真的启动了。有时候你以为调了start(),但可能因为某些条件没满足,线程根本没跑起来。打印一下状态,问题一目了然。

2.7 小结

这一节咱们把继承QThread的完整流程走了一遍。从创建类、重写run()、调用start(),到管理线程的生命周期。说白了,核心就三件事:

  1. 写一个类继承QThread,重写run()方法
  2. 调用start(),而不是直接调run()
  3. 记得wait(),让线程安全退出

嗯,这些基础打牢了,后面学信号槽、线程同步、线程池,都会顺手很多。下一节咱们会深入探讨run()里到底能写什么、不能写什么,以及如何优雅地让线程停下来。

记住:多线程编程,安全第一。宁可多写几行等待代码,也别让程序崩溃在用户面前。