993aa30a1c
实测单帧 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>
173 lines
7.3 KiB
C++
173 lines
7.3 KiB
C++
// ============================================================================
|
||
// mandelbrot_widget.cpp —— 交互与渲染调度实现(详见 .h)
|
||
// ============================================================================
|
||
#include "mandelbrot_widget.h"
|
||
|
||
#include <QHBoxLayout>
|
||
#include <QLabel>
|
||
#include <QMouseEvent>
|
||
#include <QPainter>
|
||
#include <QPushButton>
|
||
#include <QResizeEvent>
|
||
#include <QThreadPool>
|
||
#include <QThread>
|
||
#include <QVBoxLayout>
|
||
#include <QWheelEvent>
|
||
|
||
MandelbrotWidget::MandelbrotWidget(QWidget* parent)
|
||
: QWidget(parent), pal_(256) {
|
||
// 线程池默认按核数;切到「单线程」模式时再 setMaxThreadCount(1)。
|
||
QThreadPool::globalInstance()->setMaxThreadCount(QThread::idealThreadCount());
|
||
threadCount_ = QThreadPool::globalInstance()->maxThreadCount();
|
||
|
||
auto* btnFreeze = new QPushButton(QStringLiteral("主线程渲染 (将冻结)"));
|
||
auto* btnToggle = new QPushButton(QStringLiteral("切换 单/多线程"));
|
||
label_ = new QLabel(QStringLiteral("就绪 · 滚轮缩放 · 左键拖拽平移"));
|
||
|
||
auto* bar = new QHBoxLayout;
|
||
bar->setContentsMargins(8, 4, 8, 4);
|
||
bar->addWidget(btnFreeze);
|
||
bar->addWidget(btnToggle);
|
||
bar->addSpacing(16);
|
||
bar->addWidget(label_);
|
||
bar->addStretch();
|
||
|
||
auto* root = new QVBoxLayout(this);
|
||
root->setContentsMargins(0, 0, 0, 0);
|
||
root->setSpacing(0);
|
||
root->addStretch(); // 画布占主体(paintEvent 画整个 widget 背景,工具条浮在底部)
|
||
root->addLayout(bar);
|
||
|
||
connect(btnFreeze, &QPushButton::clicked, this, &MandelbrotWidget::onRenderOnMainThread);
|
||
connect(btnToggle, &QPushButton::clicked, this, &MandelbrotWidget::onToggleThreadMode);
|
||
}
|
||
|
||
// ---- 绘图:只在 paintEvent 内对【窗口 widget】构造 QPainter(核心踩坑点 1)----
|
||
void MandelbrotWidget::paintEvent(QPaintEvent*) {
|
||
QPainter p(this);
|
||
if (!pixmap_.isNull()) {
|
||
p.drawImage(0, 0, pixmap_);
|
||
} else {
|
||
p.fillRect(rect(), Qt::black);
|
||
}
|
||
}
|
||
|
||
// ---- 事件处理:滚轮缩放(以鼠标点为锚,缩放后该点复坐标不变)----
|
||
void MandelbrotWidget::wheelEvent(QWheelEvent* e) {
|
||
const int dy = e->angleDelta().y();
|
||
if (dy == 0) { QWidget::wheelEvent(e); return; }
|
||
const double factor = (dy > 0) ? 0.8 : 1.25; // 上滚放大、下滚缩小
|
||
|
||
const QPointF m = e->position();
|
||
const double mx = vp_.centerX + (m.x() - width() / 2.0) * vp_.scale;
|
||
const double my = vp_.centerY + (m.y() - height() / 2.0) * vp_.scale;
|
||
vp_.scale *= factor;
|
||
vp_.centerX = mx - (m.x() - width() / 2.0) * vp_.scale;
|
||
vp_.centerY = my - (m.y() - height() / 2.0) * vp_.scale;
|
||
scheduleRender();
|
||
}
|
||
|
||
// ---- 事件处理:左键拖拽平移 ----
|
||
void MandelbrotWidget::mousePressEvent(QMouseEvent* e) {
|
||
if (e->button() == Qt::LeftButton) { dragging_ = true; lastMouse_ = e->pos(); }
|
||
QWidget::mousePressEvent(e);
|
||
}
|
||
void MandelbrotWidget::mouseMoveEvent(QMouseEvent* e) {
|
||
if (dragging_) {
|
||
const QPoint d = e->pos() - lastMouse_;
|
||
lastMouse_ = e->pos();
|
||
vp_.centerX -= d.x() * vp_.scale;
|
||
vp_.centerY -= d.y() * vp_.scale;
|
||
scheduleRender();
|
||
}
|
||
QWidget::mouseMoveEvent(e);
|
||
}
|
||
void MandelbrotWidget::mouseReleaseEvent(QMouseEvent* e) {
|
||
if (e->button() == Qt::LeftButton) dragging_ = false;
|
||
QWidget::mouseReleaseEvent(e);
|
||
}
|
||
|
||
void MandelbrotWidget::resizeEvent(QResizeEvent* e) {
|
||
QWidget::resizeEvent(e);
|
||
scheduleRender(); // 首次 show 与窗口缩放都经此触发首帧
|
||
}
|
||
|
||
// ---- 多线程调度:切条带丢池,渐进回传 ----
|
||
void MandelbrotWidget::scheduleRender() {
|
||
++epoch_; // 令所有在途旧任务的结果作废
|
||
vp_.width = width();
|
||
vp_.height = height();
|
||
if (vp_.width <= 0 || vp_.height <= 0) return;
|
||
|
||
pixmap_ = QImage(vp_.width, vp_.height, QImage::Format_RGB32);
|
||
stripsTotal_ = multiThread_ ? QThread::idealThreadCount() * 2 : 1; // 多线程:2×核数均衡负载
|
||
stripsDone_ = 0;
|
||
timer_.start();
|
||
|
||
const int N = stripsTotal_;
|
||
for (int i = 0; i < N; ++i) {
|
||
const int y0 = vp_.height * i / N;
|
||
const int y1 = vp_.height * (i + 1) / N;
|
||
auto* task = new MandelbrotStripTask(epoch_, i, vp_, pal_, y0, y1);
|
||
// 排队连接:stripReady 自动调度回主线程(接收者 this 所在线程)执行 onStripReady。
|
||
connect(task, &MandelbrotStripTask::stripReady, this, &MandelbrotWidget::onStripReady);
|
||
QThreadPool::globalInstance()->start(task);
|
||
}
|
||
}
|
||
|
||
void MandelbrotWidget::onStripReady(quint64 epoch, int stripIndex, QImage strip) {
|
||
if (epoch != epoch_) return; // 过期结果丢弃(防竞态覆盖新画面)
|
||
if (!strip.isNull()) {
|
||
QPainter p(&pixmap_); // 对【离屏 QImage】构造 QPainter——合法(非窗口 widget)
|
||
p.drawImage(0, vp_.height * stripIndex / stripsTotal_, strip);
|
||
}
|
||
++stripsDone_;
|
||
update(); // 渐进刷新:能看到条带自上而下并行填满
|
||
if (stripsDone_ >= stripsTotal_) {
|
||
label_->setText(QString(QStringLiteral("%1 · %2 ms · %3 线程"))
|
||
.arg(multiThread_ ? QStringLiteral("多线程渲染")
|
||
: QStringLiteral("单线程渲染"))
|
||
.arg(int(timer_.elapsed()))
|
||
.arg(threadCount_));
|
||
}
|
||
}
|
||
|
||
// ---- 卡死演示:在 GUI 线程同步算整帧,不分块、不丢池 → 事件循环停摆 → 界面冻结 ----
|
||
void MandelbrotWidget::onRenderOnMainThread() {
|
||
++epoch_; // 让在途后台任务的结果作废,避免覆盖本帧
|
||
vp_.width = width();
|
||
vp_.height = height();
|
||
if (vp_.width <= 0 || vp_.height <= 0) return;
|
||
|
||
timer_.start();
|
||
pixmap_ = QImage(vp_.width, vp_.height, QImage::Format_RGB32);
|
||
const int H = vp_.height;
|
||
const int chunk = 24; // 分块调 renderStrip 只为复用同一纯函数
|
||
// 自适应加重:单帧仅约几十毫秒(实测 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 · %2 轮"))
|
||
.arg(int(timer_.elapsed())).arg(frames));
|
||
}
|
||
|
||
// ---- 单/多线程切换:复用同一套分块逻辑,仅切线程池大小 ----
|
||
void MandelbrotWidget::onToggleThreadMode() {
|
||
multiThread_ = !multiThread_;
|
||
QThreadPool::globalInstance()->setMaxThreadCount(
|
||
multiThread_ ? QThread::idealThreadCount() : 1);
|
||
threadCount_ = QThreadPool::globalInstance()->maxThreadCount();
|
||
scheduleRender();
|
||
}
|