news 2026/10/18 6:36:03

我对Qt多线程程序一直一知半解,你愿意给我讲讲吗

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
我对Qt多线程程序一直一知半解,你愿意给我讲讲吗

当然。我们先从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(); }

运行后,按下面的顺序观察:

  1. 不点按钮时:计数器持续增加,窗口正常响应。
  2. 点击按钮后:计数器停止约 5 秒,再点击界面也无法及时响应(无法移动窗体)。
  3. 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 秒的任务。
  • 通过信号和槽更新进度条。
  • 主线程计数器持续更新,方便对比上一个卡顿示例。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 6:34:06

57.实战PostgreSQL-Milvus-Redis-MinIO构建企业知识库

实战&#xff1a;PostgreSQL Milvus Redis MinIO 构建企业知识库 码海寻道 大模型、智能体与 RAG 工程组件系列第 57 篇 当知识库进入企业生产环境&#xff0c;通常需要把业务事实、向量索引、原始文件和缓存分开。本文给出一套组件组合和关键数据流&#xff0c;重点放在职…

作者头像 李华
网站建设 2026/10/18 6:33:32

Rust重写文字处理内核,开放AI可调用的文档处理库wordcraft实践

最近在 GitHub 上看到一个很有意思的项目&#xff0c;叫 wordcraft&#xff0c;一句话概括就是&#xff1a;用 Rust 重写了一套兼容主流文档格式的文字处理内核&#xff0c;并且把编辑能力以库和接口的形式开放出来&#xff0c;让 AI Agent 可以直接调用。这个方向的想象力很大…

作者头像 李华
网站建设 2026/10/18 6:33:03

三层交换与链路聚合如何协同工作:原理、配置与排错指南

你是在内网搭了个服务器&#xff0c;想让全公司都能飞快访问&#xff0c;却发现交换机端口不够用、链路带宽也吃紧。或者你刚接手一台二手三层交换机&#xff0c;想把它当核心使&#xff0c;配上链路聚合&#xff0c;结果接口状态怎么都不对&#xff0c;业务就是不通。我干网络…

作者头像 李华
网站建设 2026/10/18 6:33:00

STM32核心板为何没反应?空白板烧录与点灯全流程解析

1. 先给结论&#xff1a;核心板带不带程序&#xff0c;得看它是"验货板"还是"空白裸板"很多刚接触STM32的朋友拿到第一块核心板时&#xff0c;都会冒出同一个疑问&#xff1a;插上电&#xff0c;板子上一点反应都没有&#xff0c;是不是坏了&#xff1f;又…

作者头像 李华
网站建设 2026/10/18 6:32:46

基于sherpa-onnx的Android离线语音识别与语音合成实战指南

早些年做语音产品&#xff0c;总觉得离线识别和合成是稀罕事&#xff0c;直到真把 sherpa-onnx 跑在 Android 上&#xff0c;才意识到本地 ASR / TTS 的门槛可以压得非常低。用云端方案当然省事&#xff0c;但网络一抖就抓瞎&#xff0c;还会带来延迟和隐私问题。这阵子我重新把…

作者头像 李华
网站建设 2026/10/18 6:32:45

滴水OS 2.0 Pre:AI原生智能座舱操作系统架构与技术解析

今年的CES2026展区里&#xff0c;智能座舱依然是绝对的流量担当&#xff0c;但真正让我停下来反复琢磨的&#xff0c;并不是哪家又堆了一块更大的屏幕&#xff0c;而是“滴水OS 2.0 Pre”这套主打“AI原生”的座舱操作系统。看了发布信息和现场演示之后&#xff0c;我的感觉是&…

作者头像 李华