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 线程的生命周期管理
线程不是启动就完事了。它有自己的生命周期:创建 → 就绪 → 运行 → 阻塞 → 终止。
我画了一张图,帮你理清这个流程:
这张图里,我特别想强调几个点:
- 创建阶段:
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(),到管理线程的生命周期。说白了,核心就三件事:
- 写一个类继承QThread,重写
run()方法 - 调用
start(),而不是直接调run() - 记得
wait(),让线程安全退出
嗯,这些基础打牢了,后面学信号槽、线程同步、线程池,都会顺手很多。下一节咱们会深入探讨run()里到底能写什么、不能写什么,以及如何优雅地让线程停下来。
记住:多线程编程,安全第一。宁可多写几行等待代码,也别让程序崩溃在用户面前。