资讯动态

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

发布时间:2026/10/18 6:36:04 来源:尧图企业网站定制
当然。我们先从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 c17 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 秒的任务。通过信号和槽更新进度条。主线程计数器持续更新方便对比上一个卡顿示例。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑