09-线程与任务:不卡 UI 的正确姿势¶
目录页:见 Qt 教程目录
目标¶
- 记住一个关键结论:UI 只能在主线程更新
- 学会最常用的后台任务写法:Worker + QThread
- 能把一个耗时操作放到后台执行,并把结果安全带回 UI
先决知识(可选)¶
- 理解信号与槽(见 04 章)
- 会写一个简单窗口(见 05 章)
1. 为什么会卡:主线程既画 UI 又做重活¶
- 专业名称:GUI Thread / Main Thread(主线程)
- 类比:收银台只有一个人。如果他一边收银(处理鼠标点击、绘制界面),一边去仓库搬货(耗时计算/网络),队伍就会卡住
- 作用:主线程负责事件循环与界面刷新
- 规则/坑点:只要你在主线程里做了耗时工作,窗口就会“无响应”
2. 一个必须背下来的规则:UI 只能在主线程改¶
结论: - 任何 QWidget/QObject 的 GUI 操作都要在主线程做
正确思路: 1. 子线程做耗时工作(计算、网络、读文件等) 2. 做完后发信号把结果送回主线程 3. 主线程的槽函数里更新 UI
3. 最常用模式:Worker + QThread(推荐入门)¶
- 专业名称:Worker Object Pattern(工作对象模式)
- 类比:你雇一个“搬运工”(worker)去做重活,但他通过对讲机(信号)向你汇报结果
- 作用:代码清晰,线程生命周期可控
- 规则/坑点:不要继承 QThread 去写业务逻辑(容易把“线程”和“任务”混在一起);更推荐把任务写在 QObject 上,然后 moveToThread
3.1 最小例子:后台计数 + UI 显示(可运行)¶
这个例子做的事: - 点击 Start 后开一个线程 - worker 每隔一会发一次 progress - UI 用标签显示进度
#include <QApplication>
#include <QLabel>
#include <QPushButton>
#include <QThread>
#include <QVBoxLayout>
#include <QWidget>
class Worker : public QObject {
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr) : QObject(parent) {}
public slots:
void run() {
for (int i = 0; i <= 100; i += 1) {
QThread::msleep(30);
emit progress(i);
}
emit done();
}
signals:
void progress(int value);
void done();
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QWidget w;
w.setWindowTitle("Thread Demo");
auto *layout = new QVBoxLayout(&w);
auto *label = new QLabel("ready");
auto *btn = new QPushButton("Start");
layout->addWidget(label);
layout->addWidget(btn);
auto *thread = new QThread(&w);
auto *worker = new Worker;
worker->moveToThread(thread);
QObject::connect(btn, &QPushButton::clicked, &w, [=] {
label->setText("running...");
thread->start();
QMetaObject::invokeMethod(worker, "run", Qt::QueuedConnection);
});
QObject::connect(worker, &Worker::progress, &w, [=](int v) {
label->setText(QString("progress=%1").arg(v));
});
QObject::connect(worker, &Worker::done, &w, [=] {
label->setText("done");
thread->quit();
});
QObject::connect(thread, &QThread::finished, worker, &QObject::deleteLater);
w.resize(320, 180);
w.show();
return app.exec();
}
#include "main.moc"
你要抓住关键线:
- worker->moveToThread(thread):worker 的槽函数会在那个线程里执行
- worker 发出的信号会自动排队回到主线程(因为接收者是 UI 对象)
- thread->finished 后 worker->deleteLater:让 worker 生命周期安全收尾
4. 什么时候不用线程:其实你只需要“异步”¶
很多新手看到“不卡 UI”就开线程,但有些事用下面方式更简单: - QTimer:把工作切成小块,分帧做 - Qt 的异步 I/O(网络请求本身是异步的,见 10 章)
结论: - 只有当你的工作真的会占用 CPU 很久,或者必须在后台等待,才考虑线程
常见坑¶
-
错误:在 worker 线程里直接操作 UI
结果:随机崩溃、难复现 bug
正确:worker 发信号回主线程,主线程更新 UI -
错误:线程结束了,worker 还在跑或对象没释放
结果:内存泄漏、退出时崩溃
正确:用thread->quit()+thread->wait()(必要时)并用deleteLater管理对象生命周期 -
错误:按钮点击多次反复 start,同一个 thread 重复启动逻辑乱
结果:行为不稳定
正确:禁用按钮直到 done,或每次创建新的线程/worker(根据需求设计)
小练习¶
练习 1:可取消任务¶
- 题目:加一个 Cancel 按钮,点击后让任务尽快停止
- 输入/输出:任务停止,UI 显示 canceled
- 约束:不能强杀线程
- 提示:给 worker 一个原子/标志位,在循环里检查
- 目标:练“可控的后台任务”
练习 2:后台加载文件¶
- 题目:选择一个大文本文件,在后台读取行数,最后显示总行数
- 输入/输出:显示行数
- 约束:读取必须在 worker 线程里
- 提示:QFile + QTextStream,读取过程中发 progress
- 目标:把线程用到真实任务
小结¶
- 主线程负责 UI 与事件循环,耗时工作会让窗口卡住
- UI 只能在主线程更新;子线程做事,信号带结果回主线程
- 入门推荐 Worker + QThread 的工作对象模式
下一篇:见 10-网络与JSON-请求接口与解析数据