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子类方式的核心逻辑:
说白了,整个流程就是:主线程创建并启动子线程,子线程在run()里干活,干完了自动退出。主线程通过wait()确认它真的结束了,再安全地清理资源。
2.7 什么时候用这种方式?
我个人觉得,继承QThread重写run()最适合以下场景:
- 任务是一次性的,不需要反复启动停止
- 任务不需要事件循环(比如纯计算、文件处理)
- 你想快速验证多线程效果,不想引入复杂机制
如果你的线程需要响应外部命令、需要反复处理多个任务,那moveToThread方式会更合适。这个我们后面会讲到。
我的建议:初学者先用继承QThread的方式跑通第一个多线程程序,感受一下线程的并行效果。等理解了线程的生命周期和信号槽通信,再去尝试其他模式。别一口吃成胖子。
好了,这一章就到这里。代码不多,但都是核心。你动手写一遍,跑起来看看打印效果,比看十遍文章都管用。