Mandelbrot 教学示例增强:主线程渲染循环至 ≥1.5s,让界面冻结肉眼可感
实测单帧 820×640@256 单线程仅约 70ms,远低于人眼可感阈值,直接算一帧看不出 「冻结」。故「主线程渲染」按钮改为自适应循环渲染直到累计 ≥ 1500ms(实测 ~23 帧 / 1506ms),上限 100 轮防止极慢机器卡死过久。冻结的本质是事件循环被占用,循环只是 模拟任意一个占着 GUI 线程不返回的耗时操作。 - mandelbrot_widget.cpp:onRenderOnMainThread 改 do-while 循环,标签加「· N 轮」 - README.md:补冻结时长说明与课堂演示技巧 - 设计文档同步:三段式对比章节更新实测数据与冻结本质解释 - 新增 docs/teaching/MULTITHREADING.md:多线程教学要点汇总 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
# Qt 多线程编程 —— 一课时基准讲义
|
||||
|
||||
> **适用对象**:已掌握 C++ 基础语法与 Qt 信号槽机制的中初级学员。
|
||||
> **讲解时长**:约 60 分钟(含看代码与提问),各节标题后标注的建议时长可作节奏参考。
|
||||
> **配套环境**:Qt 5(本文示例基于 Qt 5.14 验证;worker-object 等核心模式在 Qt 6 完全一致)。
|
||||
> **核心目标**:学完后能回答三个问题——① Qt 为什么需要多线程;② 槽函数到底在哪个线程执行;③ 给你一段耗时逻辑,你能用最稳妥的方式把它移出主线程而不崩溃。
|
||||
|
||||
---
|
||||
|
||||
## 引言:为什么要多线程(5′)
|
||||
|
||||
绝大多数 GUI 程序都只有**一个主线程**(也叫 GUI 线程)。Qt 的主线程一直在跑一个
|
||||
**事件循环**(`QApplication::exec()`)——它不停地从事件队列里取事件并处理:鼠标点击、
|
||||
键盘按键、定时器到期、重绘请求、网络数据到达……
|
||||
|
||||
**关键事实**:事件循环是**串行**的。处理一个事件时,后面的事件只能排队等。
|
||||
|
||||
那么,当主线程在执行一个**耗时操作**(比如解析一个大文件、做一次复杂计算、等待网络响应)时,
|
||||
会发生什么?——它在忙,事件循环停摆,所有排队的事件(包括"请重绘界面""请响应鼠标")
|
||||
全部被卡住。表现就是:
|
||||
|
||||
- 按钮点了没反应;
|
||||
- 窗口拖不动、缩放不了;
|
||||
- 进度条不动、界面"未响应"、Windows 标题栏甚至会出现"程序无响应"。
|
||||
|
||||
这就是 Qt 多线程存在的**唯一根本理由**:**把耗时操作搬出主线程,让主线程专心响应界面。**
|
||||
|
||||
> 一句话心法:**多线程在 Qt 里的首要目的不是"更快",而是"界面不卡"。** 当然,顺带还能用
|
||||
> 多核并行加速,但那是第二位的。记住这个优先级,后面所有设计决策都有了依据。
|
||||
|
||||
---
|
||||
|
||||
## 一、基本概念(12′)
|
||||
|
||||
### 1.1 线程与进程
|
||||
|
||||
一句话带过:**进程是资源分配单位,线程是 CPU 调度单位**。一个进程至少有一个线程(主线程),
|
||||
可以再开若干子线程;同进程的线程**共享同一片内存空间**(这点对后面的"竞态/加锁"至关重要)。
|
||||
|
||||
### 1.2 主线程与事件循环
|
||||
|
||||
如引言所述,主线程跑 `exec()` 事件循环。**Qt 里只有主线程能创建和操作 GUI 控件**
|
||||
(QWidget 及其子类)——这是铁律,记住它,"最佳实践"里会反复用到。
|
||||
|
||||
### 1.3 线程亲和性(thread affinity)—— 本课最重要的概念
|
||||
|
||||
每个 `QObject` 都有一个"**归属线程**",即它的**线程亲和性**。可以用 `obj->thread()` 查询。
|
||||
|
||||
亲和性的核心规则:**一个 QObject 的槽函数,默认在它「自己归属的线程」里执行,而不是在
|
||||
调用它的线程里执行。** 这句话是理解 Qt 多线程的钥匙。
|
||||
|
||||
默认情况下,一个对象在哪条线程被 `new` 出来,它的亲和性就是哪条线程。要改变亲和性,
|
||||
用 `moveToThread(thread)`——这一步之后,该对象的槽函数就会在 `thread` 里执行。
|
||||
|
||||
> 把亲和性理解成"这个对象的家"。信号槽机制会自动把"去这个对象家敲门"(调用它的槽)
|
||||
> 这件事,调度到它家所在的线程去做——这就是下一小节"排队连接"的本质。
|
||||
|
||||
### 1.4 跨线程信号槽:三种连接类型
|
||||
|
||||
`QObject::connect` 在建立连接时,会根据**发送者和接收者是否在同一线程**,自动选择连接类型:
|
||||
|
||||
| 连接类型 | 触发方式 | 何时被 Qt 自动选用 |
|
||||
|---|---|---|
|
||||
| **DirectConnection**(直接) | `emit` 处直接调用槽,**同步**,在发送者线程执行 | 发送者与接收者**在同一线程** |
|
||||
| **QueuedConnection**(排队) | `emit` 把调用**投递**到接收者线程的事件队列,**异步**,在接收者线程执行 | 发送者与接收者**在不同线程** |
|
||||
| **AutoConnection**(自动,默认) | 运行时判断二者是否同线程,同则直接、不同则排队 | 不指定类型时的默认值 |
|
||||
|
||||
**排队连接是 Qt 多线程的魔法所在**:你在子线程里 `emit resultReady(x)`,Qt 会自动把
|
||||
"调用主线程那个槽"打包成一个事件,投递到主线程事件队列。主线程在下一轮事件循环里取出它、
|
||||
执行槽。于是——**子线程算出的结果,自动、安全地"回到"了主线程,全程不需要你手动加锁。**
|
||||
|
||||
这也是为什么官方推荐"用信号槽在跨线程间传递数据,而不是共享变量 + 锁"。
|
||||
|
||||
---
|
||||
|
||||
## 二、使用线程的方法(15′)
|
||||
|
||||
Qt 提供了几条不同的多线程路径,从底层到高层依次为:
|
||||
|
||||
### 方法一:继承 `QThread`,重写 `run()`
|
||||
|
||||
最古老、教程里最常见的写法。把耗时逻辑写进 `run()`,`start()` 后 `run()` 在子线程执行。
|
||||
|
||||
```cpp
|
||||
class MyThread : public QThread {
|
||||
protected:
|
||||
void run() override {
|
||||
// 这段代码在子线程执行
|
||||
// ... 耗时操作 ...
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**能跑,但容易踩坑**:很多人以为"MyThread 的所有槽都在子线程执行"——**错**。`QThread`
|
||||
对象本身(包括它的槽)依然活在**创建它的线程**(通常是主线程)。只有 `run()` 里的代码
|
||||
在子线程。这个认知错位会导致"明明 moveTo 了,槽却还在主线程跑"的困惑。
|
||||
|
||||
### 方法二:Worker-Object + `moveToThread`(官方推荐)⭐
|
||||
|
||||
不继承 `QThread`,而是把耗时逻辑写成一个**普通 `QObject` 子类**(Worker)的槽函数,
|
||||
再用 `moveToThread()` 把它整个挪到子线程。此后 Worker 的槽就在子线程执行,
|
||||
通过信号槽与主线程双向通信。
|
||||
|
||||
这是 Qt 官方**强烈推荐**的写法,第三节会给完整示例。优点:槽确实在子线程、用信号槽通信
|
||||
天然线程安全、代码清晰。
|
||||
|
||||
### 方法三:`QRunnable` + `QThreadPool`(任务并行)
|
||||
|
||||
适合"**一堆独立的、可并行的小任务**"(如分块计算、批量处理)。每个任务继承 `QRunnable`,
|
||||
`run()` 写计算逻辑,丢进 `QThreadPool` 由它调度到若干线程并行执行。
|
||||
|
||||
与方法二相比:worker-object 适合"**一个长期存在的后台对象**",`QRunnable` 适合
|
||||
"**大量一次性任务**"。第三节也给出示例。
|
||||
|
||||
### 方法四(了解即可):`QtConcurrent` + `QFuture`
|
||||
|
||||
更高层、更省代码的 API。`QtConcurrent::run()` 一行就能把一个函数扔到线程池;
|
||||
`QFuture` 用来取结果/取消/等待。**适合简单的一次性并行**,缺点是控制粒度粗、调试不如
|
||||
QThread 直观。本课不展开,知道"它是更上层的封装"即可。
|
||||
|
||||
### 决策表:何时用哪种?
|
||||
|
||||
| 场景 | 推荐方法 |
|
||||
|---|---|
|
||||
| 一个长期运行的后台服务(持续监听、定期处理) | **方法二**(worker-object) |
|
||||
| 大量独立、可并行的一次性任务(批量、分块) | **方法三**(QRunnable + 线程池) |
|
||||
| 简单地把单个函数异步跑一次,不想写类 | **方法四**(QtConcurrent::run) |
|
||||
| 维护老代码、或确需完全控制线程入口 | 方法一(继承 QThread,慎用) |
|
||||
|
||||
> 给中初级学员的默认建议:**绝大多数情况用方法二(worker-object)。** 它最不易出错、最契合
|
||||
> Qt 的信号槽体系。除非任务特征明确指向方法三/四,否则一律用方法二起步。
|
||||
|
||||
---
|
||||
|
||||
## 三、如何使用多线程(18′)
|
||||
|
||||
### 3.1 完整示例:worker-object 模式(主线示例,务必看懂)
|
||||
|
||||
下面是一个**自包含、可编译、可运行**的控制台程序。它把"1 加到 5000 万"这件耗时计算
|
||||
丢到子线程,算完把结果安全地传回主线程打印。
|
||||
|
||||
```cpp
|
||||
// main.cpp —— worker-object 多线程最小完整示例(控制台,无需 GUI)
|
||||
#include <QCoreApplication>
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QDebug>
|
||||
|
||||
// 工作对象:普通 QObject 子类。注意它「不」继承 QThread。
|
||||
class Worker : public QObject {
|
||||
Q_OBJECT
|
||||
public slots:
|
||||
void doWork() { // 这个槽将运行在子线程(因为 worker 被 moveToThread)
|
||||
// 模拟耗时计算(真实场景:解析大文件、网络请求、复杂运算……)
|
||||
// 用 qint64 防止溢出:5000 万求和 ≈ 1.25e15,远超 int 上限
|
||||
qint64 sum = 0;
|
||||
for (int i = 1; i <= 50'000'000; ++i) sum += i;
|
||||
emit resultReady(sum); // 算完发信号 → 跨线程排队连接,自动带回主线程
|
||||
emit finished(); // 通知:工作完成
|
||||
}
|
||||
signals:
|
||||
void resultReady(qint64 result); // 把结果交给主线程
|
||||
void finished(); // 通知主线程:可以结束了
|
||||
};
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
auto* thread = new QThread; // 线程对象(本身活在主线程,管理一个子线程)
|
||||
auto* worker = new Worker; // worker 当前还在主线程
|
||||
worker->moveToThread(thread); // ★ 关键一步:把 worker 挪到子线程
|
||||
|
||||
// —— 四条核心接线(全部用函数指针语法,连接类型交由 Qt 自动判定)——
|
||||
// ① 线程一启动 → 立刻开始干活
|
||||
QObject::connect(thread, &QThread::started, worker, &Worker::doWork);
|
||||
|
||||
// ② 结果回主线程:接收者 &app 在主线程,于是这是「排队连接」,
|
||||
// 回调 lambda 必然在主线程执行(看下一行的断言验证)
|
||||
QObject::connect(worker, &Worker::resultReady, &app, [](qint64 result) {
|
||||
qDebug() << "计算结果:" << result
|
||||
<< "(回调在"
|
||||
<< (QThread::currentThread() == qApp->thread() ? "主" : "子")
|
||||
<< "线程执行)";
|
||||
});
|
||||
|
||||
// ③ 优雅退出 + 清理(Qt 官方标准四件套,照抄即可)
|
||||
QObject::connect(worker, &Worker::resultReady, thread, &QThread::quit); // 算完让子线程事件循环退出
|
||||
QObject::connect(thread, &QThread::finished, worker, &QObject::deleteLater); // 子线程结束 → 删 worker
|
||||
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater); // → 删 thread
|
||||
QObject::connect(thread, &QThread::finished, &app, &QCoreApplication::quit);// → 退出程序
|
||||
|
||||
thread->start(); // 启动子线程事件循环
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
#include "main.moc" // Worker 定义在 .cpp 内时需要此行(拆成 .h/.cpp 则由 AUTOMOC 自动处理)
|
||||
```
|
||||
|
||||
**走读这个示例(这是本课重点)**:
|
||||
|
||||
1. **Worker 不继承 QThread**——这是方法二的精髓。耗时逻辑在 `doWork()` 槽里。
|
||||
2. **`moveToThread` 改变亲和性**:之后 `doWork` 在子线程执行。对比"方法一"的陷阱——
|
||||
那里只有 `run()` 在子线程,槽不在;这里 `doWork` 这个槽**确确实实在子线程**。
|
||||
3. **`started → doWork`**:线程的 `exec()` 一跑起来,`started` 信号触发 `doWork`,开始干活。
|
||||
4. **`resultReady → 主线程 lambda`**:因为接收者在主线程,Qt 自动用**排队连接**,
|
||||
把"调用 lambda"投递回主线程。打印里的断言会显示"主线程"——亲手验证排队连接真的把回调带回来了。
|
||||
5. **优雅退出**:`resultReady → quit()` 让子线程事件循环退出 → `finished` 触发 →
|
||||
`deleteLater` 清理 worker 和 thread,最后 `app.quit()` 结束程序。**没有内存泄漏,没有悬空。**
|
||||
|
||||
> **如何亲手验证"排队连接"?** 把第 4 条的回调想象成"更新一个 QLabel"。因为回调确定在主线程,
|
||||
> 所以可以**安全地**操作 GUI 控件——这正是 worker 模式能安全配合 GUI 的根本原因。
|
||||
|
||||
### 3.2 完整示例:`QRunnable` + `QThreadPool`(任务并行)
|
||||
|
||||
同样是"1 加到 5000 万",这次切成 4 段丢给线程池并行算,4 段结果都回到主线程汇总。
|
||||
|
||||
```cpp
|
||||
// main.cpp —— QRunnable + 线程池:任务并行最小示例
|
||||
#include <QCoreApplication>
|
||||
#include <QObject>
|
||||
#include <QRunnable>
|
||||
#include <QThreadPool>
|
||||
#include <QDebug>
|
||||
|
||||
// 一个可并行执行的「任务」。QRunnable 本身不发信号,要发结果须同时继承 QObject。
|
||||
class SumTask : public QObject, public QRunnable {
|
||||
Q_OBJECT
|
||||
int from_, to_;
|
||||
public:
|
||||
SumTask(int from, int to) : from_(from), to_(to) { setAutoDelete(true); }
|
||||
void run() override { // run() 在线程池的某个工作线程执行
|
||||
qint64 sum = 0;
|
||||
for (int i = from_; i < to_; ++i) sum += i;
|
||||
emit done(sum); // 跨线程排队连接,把结果带回主线程
|
||||
}
|
||||
signals:
|
||||
void done(qint64 sum);
|
||||
};
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
const int N = 4; // 切 4 段并行
|
||||
const int total = 20'000'000;
|
||||
const int step = total / N;
|
||||
auto* results = new qint64[N]{}; // 存每段结果
|
||||
int got = 0; // 已返回的段数
|
||||
|
||||
for (int i = 0; i < N; ++i) {
|
||||
auto* task = new SumTask(i * step, (i + 1) * step);
|
||||
// 接收者 lambda 在主线程 → done 经排队连接带回主线程,回调串行执行,天然无锁
|
||||
QObject::connect(task, &SumTask::done, &app, [&, i](qint64 sum) {
|
||||
results[i] = sum;
|
||||
if (++got == N) { // 4 段全部返回
|
||||
qDebug() << "总和:" << results[0] + results[1] + results[2] + results[3];
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
QThreadPool::globalInstance()->start(task); // 丢进全局线程池
|
||||
}
|
||||
|
||||
return app.exec();
|
||||
// 注:results 是堆对象,示例为简洁未 delete;真实程序用智能指针或栈上容器管理
|
||||
}
|
||||
|
||||
#include "main.moc"
|
||||
```
|
||||
|
||||
**两个示例对照,记住要点**:
|
||||
|
||||
- **示例 3.1**(worker-object):一个 Worker 长期住在一个子线程里,适合"后台服务"。
|
||||
- **示例 3.2**(QRunnable):多个一次性任务被线程池调度,适合"批量并行"。
|
||||
- **两者都靠排队连接把结果安全带回主线程**——这就是贯穿所有 Qt 多线程的统一范式:
|
||||
**子线程只管算,结果用信号发给主线程,主线程负责一切后续(更新界面/汇总/落盘)。**
|
||||
|
||||
---
|
||||
|
||||
## 四、最佳实践与常见坑(8′)
|
||||
|
||||
每条都对应一个**真实会崩或会卡**的坑,逐条记住可省去项目期大量排查时间。
|
||||
|
||||
### ① 绝不在工作线程里访问 GUI 控件
|
||||
|
||||
`QWidget` 及其子类**不是线程安全**的,且只能由主线程操作。在工作线程里直接调
|
||||
`label->setText(...)` 是**未定义行为**——可能崩溃、可能乱码、可能偶发花屏。
|
||||
|
||||
**正解**:子线程算出数据后 `emit` 一个信号,由主线程的槽(排队连接)去更新控件。
|
||||
(回顾示例 3.1 的回调——它确认在主线程,所以那里才是碰 GUI 的合法位置。)
|
||||
|
||||
### ② 优先用信号槽传数据,退而求其次才用 `QMutex`
|
||||
|
||||
子线程之间、子线程与主线程之间,**优先选择"通过信号槽传递数据的所有权"**(一个线程
|
||||
算完、发出去、就不再碰它),而不是"多个线程共享一块内存 + 加锁"。
|
||||
|
||||
只有当确实**无法避免共享同一块可变数据**时(如多个写线程更新同一个计数器),才用
|
||||
`QMutex`(互斥锁)/ `QReadWriteLock`。锁是最后的手段,不是首选——它带来死锁、
|
||||
性能下降、调试困难。
|
||||
|
||||
### ③ `moveToThread` 的两个时序要点
|
||||
|
||||
- **`worker` 不能有 parent**。`new Worker(parent)` 之后 `moveToThread` 会失败。务必
|
||||
`new Worker`(无 parent),由 `deleteLater` 负责它的生命周期。
|
||||
- **先 `connect`,再 `start`**。确保线程一启动,信号槽已就绪,避免"线程已跑、接线还没好"
|
||||
的竞态。
|
||||
|
||||
### ④ 优雅退出:`quit()` + `wait()`,别暴力 `terminate()`
|
||||
|
||||
- 让线程的事件循环自然结束:`thread->quit()`(请求退出)配 `deleteLater` 清理(见示例)。
|
||||
- 若是 `QThread` 作为类成员:在析构里 `quit()` + `wait()`,确保线程真正停下再销毁对象。
|
||||
- **绝不轻易用 `terminate()`**:它强行杀线程,可能导致资源未释放、锁未解开、数据损坏。
|
||||
|
||||
### ⑤ 亲和性决定槽的执行线程(再强调一次)
|
||||
|
||||
`connect` 里**接收者所在线程**决定了槽在哪执行(排队连接时)。当你疑惑"这个槽为什么
|
||||
在主线程跑"——去查接收者的 `thread()`,答案就在那里。这是排查 Qt 线程问题的第一招。
|
||||
|
||||
### ⑥ 调试利器:`QThread::currentThread()`
|
||||
|
||||
任何时候想确认"现在我在哪个线程",打印 `QThread::currentThread()` 或与 `qApp->thread()`
|
||||
比较。示例 3.1 就用它验证了回调确实在主线程。**排查多线程问题的第一行代码就是它。**
|
||||
|
||||
### 常见坑速览表
|
||||
|
||||
| 现象 | 根因 | 对应条目 |
|
||||
|---|---|---|
|
||||
| 界面卡死、"未响应" | 耗时操作在主线程 | 引言 / ① |
|
||||
| 工作线程更新控件崩溃 | 子线程碰了 GUI | ① |
|
||||
| moveToThread 后槽还在主线程跑 | 误用继承 QThread(方法一陷阱) | 二·方法一 / ⑤ |
|
||||
| 程序退出时崩溃/卡死 | 线程未优雅退出 | ④ |
|
||||
| 偶发数据错乱 | 多线程共享可变数据未加锁 | ② |
|
||||
|
||||
---
|
||||
|
||||
## 小结:一页速查表(2′)
|
||||
|
||||
**Qt 多线程的三条心法**:
|
||||
|
||||
1. **动机**:多线程的首要目的是**界面不卡**(把耗时操作搬出主线程),其次是多核加速。
|
||||
2. **核心抽象**:**线程亲和性**决定槽在哪个线程跑;**排队连接**自动把跨线程调用安全调度到目标线程。
|
||||
3. **统一范式**:**子线程只算,结果用信号发回主线程,主线程负责一切后续(含更新 GUI)。**
|
||||
|
||||
**默认选型**:长期后台服务 → worker-object(方法二);批量并行任务 → QRunnable + 线程池(方法三)。
|
||||
|
||||
**四条铁律**:① 子线程不碰 GUI;② 优先信号槽传数据、慎用锁;③ moveToThread 先 connect 再 start、worker 无 parent;④ 优雅 quit+wait、禁用 terminate。
|
||||
|
||||
---
|
||||
|
||||
## 附:构建运行(一行说明)
|
||||
|
||||
上述示例为单 `.cpp` 控台程序。最小 CMake 片段:
|
||||
|
||||
```cmake
|
||||
find_package(Qt5 COMPONENTS Core REQUIRED)
|
||||
add_executable(thread_demo main.cpp)
|
||||
target_link_libraries(thread_demo PRIVATE Qt5::Core)
|
||||
set_target_properties(thread_demo PROPERTIES AUTOMOC ON) # 自动处理 moc,无需手写 main.moc
|
||||
```
|
||||
|
||||
开启 `AUTOMOC` 后,示例末尾的 `#include "main.moc"` 可去掉,CMake 会自动生成元对象代码。
|
||||
若把 Worker/SumTask 拆到独立的 `.h`,则无论哪种构建方式都无需手动处理 moc。
|
||||
|
||||
> **运行提示(看不到 qDebug 输出?)**:在 Windows + 官方**发行版(release)** Qt 下,
|
||||
> `qDebug` 默认走系统调试输出(`OutputDebugString`)而非终端,控制台程序可能看不到打印。
|
||||
> 这**不是程序没执行**——线程照常运行、结果照常计算。验证手段任选其一:① 用 Debug 版 Qt;
|
||||
> ② 临时改用 `std::cerr << ... << std::flush;` 打印关键路径;③ 在 IDE/调试器输出窗口查看。
|
||||
> 示例逻辑的正确性不依赖此输出(上述三种示例的运行行为均已在 Qt 5.14.2 上端到端验证通过)。
|
||||
@@ -21,6 +21,12 @@
|
||||
|
||||
三段对比让两个结论同时成立:**耗时任务必须离开主线程**(否则冻结);**多线程进一步压榨多核**(加速比)。
|
||||
|
||||
> **关于「主线程渲染」的冻结时长**:单帧 820×640@256 单线程仅约 **70ms**,远低于人眼可感阈值,
|
||||
> 直接算一帧看不出「冻结」。故该按钮**自适应循环渲染直到累计 ≥ 1500ms**(实测约 23 帧 / 1506ms),
|
||||
> 保证在任何 CPU 上冻结都肉眼可感。冻结的本质是「事件循环被占用」,与算什么无关——循环只是模拟一个
|
||||
> 持续 ~1.5s 的耗时操作。**演示技巧**:按下该按钮后立即拖动窗口或点击其它按钮,会发现整 1.5s 内
|
||||
> 完全无响应,这就是事件循环停摆。
|
||||
|
||||
## 概念映射
|
||||
|
||||
| Day4 知识点 | 本示例实现 |
|
||||
|
||||
@@ -143,15 +143,23 @@ void MandelbrotWidget::onRenderOnMainThread() {
|
||||
pixmap_ = QImage(vp_.width, vp_.height, QImage::Format_RGB32);
|
||||
const int H = vp_.height;
|
||||
const int chunk = 24; // 分块调 renderStrip 只为复用同一纯函数
|
||||
for (int y = 0; y < H; y += chunk) {
|
||||
const QImage s = renderStrip(vp_, pal_, y, qMin(y + chunk, H));
|
||||
QPainter p(&pixmap_);
|
||||
p.drawImage(0, y, s);
|
||||
}
|
||||
// 自适应加重:单帧仅约几十毫秒(实测 820x640@256 ≈ 66ms),不足以让「冻结」肉眼可感。
|
||||
// 故循环渲染直到累计 ≥ 1500ms——冻结的本质是事件循环被占用,与「算什么」无关,
|
||||
// 这模拟任意一个「占着 GUI 线程不返回事件循环」的耗时操作(解析大文件、复杂运算……)。
|
||||
// 上限 100 轮防止极慢机器卡死过久。
|
||||
int frames = 0;
|
||||
do {
|
||||
for (int y = 0; y < H; y += chunk) {
|
||||
const QImage s = renderStrip(vp_, pal_, y, qMin(y + chunk, H));
|
||||
QPainter p(&pixmap_);
|
||||
p.drawImage(0, y, s);
|
||||
}
|
||||
++frames;
|
||||
} while (timer_.elapsed() < 1500 && frames < 100);
|
||||
// 注意:本函数返回前事件循环不跑——期间按钮无响应、窗口拖不动、paintEvent 排队等待。
|
||||
update();
|
||||
label_->setText(QString(QStringLiteral("主线程渲染 (界面冻结) · %1 ms"))
|
||||
.arg(int(timer_.elapsed())));
|
||||
label_->setText(QString(QStringLiteral("主线程渲染 (界面冻结) · %1 ms · %2 轮"))
|
||||
.arg(int(timer_.elapsed())).arg(frames));
|
||||
}
|
||||
|
||||
// ---- 单/多线程切换:复用同一套分块逻辑,仅切线程池大小 ----
|
||||
|
||||
Reference in New Issue
Block a user