当然。我们先从Qt 为什么需要多线程,以及 QWidget 和线程是什么关系讲起,再看一个小例子。
你可以先记住一句话:耗时任务交给工作线程,界面更新交给主线程,两边通过信号和槽传递结果。
1. 为什么界面会“卡死”?
一个 Qt Widgets 程序启动后,通常会在主线程执行:
int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget window; window.show(); return app.exec(); }这里的app.exec()会启动事件循环。鼠标点击、键盘输入、窗口重绘等事情,都需要主线程不断处理。
如果你在按钮的槽函数中做耗时工作:
connect(button, &QPushButton::clicked, this, [] { QThread::sleep(5); // 模拟耗时任务 });主线程这 5 秒一直在执行sleep(),没空处理界面事件,于是窗口无法及时响应,表现为卡死。
槽函数不会自动在新线程运行。普通按钮点击所连接的槽,通常仍在主线程执行。
示例:
QT += widgets CONFIG += c++17 TEMPLATE = app TARGET = GuiBlockingDemo SOURCES += main.cpp#include <QApplication> #include <QDebug> #include <QLabel> #include <QPushButton> #include <QThread> #include <QTimer> #include <QVBoxLayout> #include <QWidget> int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget window; window.setWindowTitle(QStringLiteral("测试:主线程阻塞")); window.resize(420, 220); auto *layout = new QVBoxLayout(&window); auto *description = new QLabel(QStringLiteral( "下面的计数器每 100 毫秒更新一次。\n" "点击按钮后,观察计数器以及窗口的响应。"), &window); auto *counterLabel = new QLabel(QStringLiteral("计数:0"), &window); auto *statusLabel = new QLabel(QStringLiteral("状态:空闲"), &window); auto *button = new QPushButton(QStringLiteral("阻塞主线程 5 秒"), &window); layout->addWidget(description); layout->addWidget(counterLabel); layout->addWidget(statusLabel); layout->addWidget(button); // 定时器和按钮槽都在主线程执行。 auto *timer = new QTimer(&window); QObject::connect(timer, &QTimer::timeout, counterLabel, [counterLabel, count = 0]() mutable { counterLabel->setText(QStringLiteral("计数:%1").arg(++count)); }); timer->start(100); QObject::connect(button, &QPushButton::clicked, &window, [statusLabel] { qDebug() << "Blocking begins, thread:" << QThread::currentThread(); statusLabel->setText(QStringLiteral("状态:正在执行耗时操作……")); // 故意阻塞主线程,观察事件循环无法处理重绘和输入的现象。 QThread::sleep(5); statusLabel->setText(QStringLiteral("状态:操作完成")); qDebug() << "Blocking ends"; }); window.show(); return app.exec(); }运行后,按下面的顺序观察:
- 不点按钮时:计数器持续增加,窗口正常响应。
- 点击按钮后:计数器停止约 5 秒,再点击界面也无法及时响应(无法移动窗体)。
- 5 秒结束后:计数器继续增加,界面恢复响应。
还有一个值得留意的现象:你可能看不到“正在执行耗时操作……”这行状态。
虽然setText()已经修改了标签内容,但屏幕上的重绘还需要主线程处理绘制事件。紧接着执行的sleep(5)阻塞了主线程,等恢复时,文字又被改成了“操作完成”。
这里的sleep()几乎不消耗 CPU,却照样让界面卡住——原因是主线程无法继续处理事件,而不一定是 CPU 忙不过来。
我想知道app.exec();这个事件循环开启,具体是在干什么?
执行到app.exec()后,主线程就不断重复这个过程:
等待事件 ↓ 收到事件(鼠标点击、键盘输入、窗口重绘、定时器等) ↓ 把事件交给对应的对象处理 ↓ 处理完成,继续等待下一个事件比如你点击一个按钮:
操作系统收到鼠标点击 → Qt 事件循环取出鼠标事件 → 交给按钮处理 → 按钮发出 clicked 信号 → 执行你连接的槽函数 → 返回事件循环,继续处理其他事件app.exec()通常会一直等到应用退出才返回,但这不代表程序卡住了。
主线程此时就在它内部处理各种事件;没有事件时通常会休眠等待,不会一直空转消耗 CPU。
这也解释了为什么在按钮的槽函数里执行耗时操作会导致界面卡顿:
connect(button, &QPushButton::clicked, [] { QThread::sleep(5); // 主线程在这里停留 5 秒 });这 5 秒里,主线程无法回到事件循环,鼠标输入、窗口重绘等事件就只能排队,所以界面看起来“卡死”。
另外,app.exec()不会自动创建新线程,它是在调用它的线程里运行事件循环,通常就是主线程。
感觉GUI框架都会涉及到事件循环这个概念,这个算是核心概念吧。
是的,事件循环是理解 GUI 框架的核心概念之一。掌握它之后,很多看似零散的问题就能联系起来:按钮为什么会响应、窗口什么时候重绘、界面为什么卡顿、后台任务如何更新界面。
它背后的原因是:GUI 程序的执行顺序,很大程度上由外部发生的事情决定。用户下一步可能点按钮、拖动窗口,也可能什么都不做。程序需要持续等待这些事情,再调用对应的代码,这就是“事件驱动”。
你可以把 GUI 程序分成三个相互配合的部分:
| 部分 | 职责 | 例子 |
|---|---|---|
| 事件 | 描述发生了什么 | 鼠标按下、键盘输入、定时器到期 |
| 事件循环 | 等待并分发事件 | 将鼠标事件交给对应控件 |
| 处理函数 | 执行具体行为 | 按钮改变状态、调用你的业务代码 |
这里最值得建立的认识是:
你写的回调或槽函数,通常是事件循环处理某个事件时调用的。它执行完并返回,事件循环才能继续处理后续事件。
事件循环 ├─ 收到点击 → 执行槽函数 → 返回 ├─ 收到重绘请求 → 绘制窗口 → 返回 ├─ 定时器到期 → 执行回调 → 返回 └─ 继续等待……因此,前面app.exec()的例子可以再深入一层理解:主线程平时运行框架的调度代码,发生事件时进入你的代码,你的代码返回后再交回框架。这种“框架决定什么时候调用你的代码”的方式,也叫控制反转。
不同 GUI 框架可能把事件循环显式暴露出来,也可能藏在应用启动流程中,但理解了这个执行模式,学习其他 GUI 框架就会容易很多。
所以app.exec();的内部就是一个循环,一直在等待事件,处理事件,继续等待事件.......
对,可以这样理解。app.exec()内部的事件循环,概念上类似下面的伪代码:
while (!收到退出请求) { 事件 = 等待并获取事件(); 分发并处理(事件); }这里有两个关键点:
- 没有事件时,会阻塞等待,通常由操作系统在有事件时唤醒,不会一直空转检查。
- 处理事件时,会调用对应的处理函数;通常要等这个函数执行完、返回,才能继续处理后续事件。
所以程序执行到:
app.exec();主线程会一直在这个函数内部运行事件循环。等到事件循环退出,app.exec()才返回,程序才继续执行它后面的代码。
多线程怎样解决它?
把耗时任务放到另一个线程:
| 主线程(GUI 线程) | 工作线程 |
|---|---|
| 处理鼠标和键盘事件 | 执行耗时计算 |
| 绘制窗口 | 处理数据 |
| 更新进度条和标签 | 发出进度、结果信号 |
两边可以并发执行,因此计算过程中界面仍能响应。
但有一个重要限制:工作线程不能直接操作 QWidget 界面对象。
例如,这种写法不正确:
// 假设这里正在工作线程中执行 label->setText("完成"); // 错误:跨线程操作界面正确做法是:工作线程发出信号,让主线程中的槽更新标签。
示例程序:
#include <QApplication> #include <QDebug> #include <QLabel> #include <QProgressBar> #include <QPushButton> #include <QThread> #include <QTimer> #include <QVBoxLayout> #include <QWidget> class Worker : public QObject { Q_OBJECT public slots: void doWork() { qDebug() << "Worker thread:" << QThread::currentThread(); // 50 个小步骤模拟约 5 秒的耗时任务。 for (int i = 1; i <= 50; ++i) { if (QThread::currentThread()->isInterruptionRequested()) { emit finished(true); return; } QThread::msleep(100); emit progress(i * 2); } emit finished(QThread::currentThread()->isInterruptionRequested()); } signals: void progress(int value); void finished(bool cancelled); }; class Window : public QWidget { public: Window() { setWindowTitle(QStringLiteral("工作线程:界面保持响应")); resize(460, 280); auto *layout = new QVBoxLayout(this); auto *description = new QLabel(QStringLiteral( "点击开始,在工作线程中执行约 5 秒的任务。\n" "观察计数器、进度条,并尝试移动窗口。"), this); auto *counter = new QLabel(QStringLiteral("界面计数:0"), this); auto *status = new QLabel(QStringLiteral("状态:尚未开始"), this); auto *bar = new QProgressBar(this); auto *start = new QPushButton(QStringLiteral("开始任务(本次运行一次)"), this); auto *cancel = new QPushButton(QStringLiteral("取消任务"), this); bar->setRange(0, 100); bar->setValue(0); cancel->setEnabled(false); layout->addWidget(description); layout->addWidget(counter); layout->addWidget(status); layout->addWidget(bar); layout->addWidget(start); layout->addWidget(cancel); auto *timer = new QTimer(this); QObject::connect(timer, &QTimer::timeout, counter, [counter, count = 0]() mutable { counter->setText(QStringLiteral("界面计数:%1").arg(++count)); }); timer->start(100); QObject::connect(start, &QPushButton::clicked, this, [this, start, cancel, status, bar] { start->setEnabled(false); cancel->setEnabled(true); status->setText(QStringLiteral("状态:执行中")); // Worker 无父对象,才能移动到工作线程。 auto *worker = new Worker; worker->moveToThread(&thread); QObject::connect(&thread, &QThread::started, worker, &Worker::doWork); // 接收对象 bar 属于 GUI 线程,更新会排队到 GUI 线程。 QObject::connect(worker, &Worker::progress, bar, &QProgressBar::setValue); QObject::connect(worker, &Worker::finished, this, [status, cancel](bool cancelled) { status->setText(cancelled ? QStringLiteral("状态:已取消") : QStringLiteral("状态:已完成")); cancel->setEnabled(false); }); QObject::connect(worker, &Worker::finished, &thread, &QThread::quit); // Qt 支持在线程结束阶段处理这个延迟删除。 QObject::connect(&thread, &QThread::finished, worker, &QObject::deleteLater); thread.start(); }); QObject::connect(cancel, &QPushButton::clicked, this, [this, status, cancel] { // 线程安全的协作取消请求;Worker 主动检查后返回。 thread.requestInterruption(); status->setText(QStringLiteral("状态:正在取消……")); cancel->setEnabled(false); }); qDebug() << "GUI thread:" << QThread::currentThread(); } ~Window() override { // 先请求任务退出,再等待线程结束,避免销毁运行中的 QThread。 thread.requestInterruption(); thread.quit(); thread.wait(); } private: QThread thread; }; int main(int argc, char *argv[]) { QApplication app(argc, argv); Window window; window.show(); return app.exec(); } // 本文件定义了带 Q_OBJECT 的类,让 qmake 生成并包含 moc 代码。 #include "main.moc"这个示例包含:
- 工作线程执行约 5 秒的任务。
- 通过信号和槽更新进度条。
- 主线程计数器持续更新,方便对比上一个卡顿示例。