From 11c77dedd35922a785faaccc38e50d43c39d089d Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Thu, 23 Jul 2026 08:51:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=8F=E7=A8=8B=E6=9E=B6=E6=9E=84=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E6=88=90=E5=8A=9F=EF=BC=8C=20=E4=B8=8B=E4=B8=80?= =?UTF-8?q?=E6=AD=A5=E5=87=86=E5=A4=87=E6=8C=87=E9=92=88=E4=BA=A4=E6=8D=A2?= =?UTF-8?q?=20=E6=97=A0=E9=94=81=E5=8C=96=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/修改指令.md | 707 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 674 insertions(+), 33 deletions(-) diff --git a/doc/修改指令.md b/doc/修改指令.md index bc871c8..6fd04ce 100644 --- a/doc/修改指令.md +++ b/doc/修改指令.md @@ -1,48 +1,689 @@ -我要重构这个多线程图表库的后台渲染缓冲模型。不要动 git。代码保持 K&R 样式,不要加无意义空行。除非必要,不要破坏已有函数签名语义;如果替换实现,旧实现直接删除,不要做兼容实现。 +# 多线程图表库后台渲染缓冲模型重构说明 -核心目标: 把每个图抽象成“状态缓冲 -> 后台 CPU 渲染 -> 颜色缓冲 -> Qt 主线程贴图”的通用模型。Qt GUI 线程和 asio -调度线程是两个不同线程。asio 调度线程是单线程,负责串行调度所有图的状态修改、任务提交、版本判断、任务完成发布、刷新请求合并。Qt -主线程不直接修改渲染状态,只把用户操作、数据输入、缩放、尺寸变化、颜色表变化等修改请求投递给 asio 调度线程。asio 调度线程串行修改 -edit_state,并递增 edit_state.version。 +## 基本要求 -后台计算使用 asio/线程池封装成 awaitable。asio 调度线程只负责任务编排,不在调度线程里做重 CPU 计算。state -> color 的重计算放到后台 -CPU 执行环境里完成。后台计算完成后,把完成事件 post 回 asio 调度线程,由 asio 调度线程发布颜色缓冲。 +我要重构这个多线程图表库的后台渲染缓冲模型。 -每个图使用两个状态缓冲: edit_state: asio 调度线程持有的最新状态。Qt 主线程收到事件后只投递修改命令,不直接写 edit_state。 -render_state: 后台渲染任务使用的稳定状态快照。提交任务时由 edit_state 复制、交换或快照生成。 +要求: -每个图使用三个颜色缓冲: front_color: Qt 主线程当前正在显示的颜色图。 ready_color: 后台已经生成完,等待 Qt 主线程消费的颜色图。 -render_color: 后台线程正在写入的颜色图。 +* 不要动 git。 +* 代码保持 K&R 样式。 +* 不要加无意义空行。 +* 除非必要,不要破坏已有函数签名语义。 +* 如果替换实现,旧实现直接删除,不要做兼容实现。 +* 能不做兼容实现就不做兼容实现,除非明确说明设计如此。 -每个缓冲对象都带 version。版本号跟随缓冲对象,不单独跟随槽位。版本比较需要封装成支持回环的函数,不要到处直接写 >。 +## 核心目标 -版本比较函数语义: version_newer (a, b) 表示 a 是否新于 b。 可以用 uint64_t,并通过有符号差值处理回环,前提是版本差距不会超过半个取值空间。 +把每个图抽象成下面这个通用模型: -颜色缓冲原则: 缓冲对象固定分配,槽位只交换指针。 所有跨线程槽位交换必须加锁。 锁内只允许做版本判断、指针交换、状态标记。 -锁内禁止做像素计算、内存分配、QImage 填充、drawImage。 大计算锁外做,锁内只换身份。 +```text +状态缓冲 -> 后台 CPU 渲染 -> 颜色缓冲 -> Qt 主线程贴图 +``` -颜色缓冲的唯一合法交换: 后台发布: ready_color <-> render_color Qt 主线程消费: front_color <-> ready_color +也就是: -不允许: front_color <-> render_color 后台线程操作 front_color Qt 主线程操作 render_color 绕过 ready_color 直接发布 +```text +State -> Color -> paintEvent drawImage +``` -后台发布流程: 后台只写 render_color。 render_color 填充完成后,完成事件回到 asio 调度线程。 asio 调度线程在 color_lock 下判断: -如果 render_color.version 新于 ready_color.version,则交换 ready_color 和 render_color。 交换后,如果 ready_color.version -新于 front_color.version,则调用 scheduler->request_update ()。 如果旧 ready_color 还没被 Qt 主线程消费,新 render_color -又完成了,直接用新 render_color 覆盖旧 ready_color。图表库追最新状态,不做帧队列。 +具体图只实现: -Qt paintEvent 流程: paintEvent 中在 color_lock 下判断: 如果 ready_color.version 新于 front_color.version,则交换 -ready_color 和 front_color。 释放锁后 drawImage (front_color)。 paintEvent 不做状态计算、不做颜色映射、不做后台等待、不提交后台任务。 +```text +State -> Color 的 CPU 渲染逻辑 +``` -刷新请求流程: 后台发布 ready_color 后,不直接在后台线程操作 QWidget。 scheduler->request_update () 负责把刷新请求投递到 Qt -GUI 线程。 使用 paint_request_pending 合并重复 update 请求。 多个图完成时合并 dirty region,不要每个图都直接 update 整个窗口。 -Qt 主线程收到刷新任务后调用 QWidget::update (region),由 Qt 稍后触发 paintEvent。 +线程调度、版本判断、缓冲交换、刷新合并逻辑放到通用框架里。 -任务提交原则: 每个图同一时间最多一个后台渲染任务。 如果任务正在 Queued 或 Running,不再提交同一图的新任务。 任务运行期间 -edit_state 又变化,只记录新版本;当前任务完成后,下一轮直接提交最新 edit_state,不排队计算中间版本。 图不可见、尺寸无效、版本没变时,不提交任务。 +## 线程模型 -状态缓冲提交流程: asio 调度线程判断 edit_state.version 是否新于 render_state.version。 如果需要提交,并且当前图没有任务在飞: -生成 render_state 快照。 render_state.version = edit_state.version。 标记任务运行。 把 render_state 对应的计算任务提交到后台 -CPU 执行环境。 CPU 任务只读取 render_state 快照并写 render_color,不直接修改 edit_state。 +Qt GUI 线程和 asio 调度线程是两个不同线程。 -需要重构出一个通用的缓冲控制抽象,具体图只实现: State -> Color 的 CPU 渲染逻辑。 线程调度、版本判断、缓冲交换、update -合并逻辑放在通用框架里。 +asio 调度线程是单线程,负责串行调度所有图的: + +```text +状态修改 +任务提交 +版本判断 +后台任务完成发布 +刷新请求合并 +``` + +Qt GUI 线程不直接修改渲染状态。 + +Qt 主线程收到用户操作、数据输入、缩放、尺寸变化、颜色表变化等事件后,只把修改请求投递给 asio 调度线程。 + +asio 调度线程串行修改 `edit_state`,并递增 `edit_state.version`。 + +## 后台计算模型 + +后台计算使用 asio / 线程池封装成 awaitable。 + +asio 调度线程只负责任务编排,不在调度线程里做重 CPU 计算。 + +`State -> Color` 的重计算放到后台 CPU 执行环境里完成。 + +后台计算完成后,把完成事件 post 回 asio 调度线程。 + +颜色缓冲发布由 asio 调度线程执行。 + +CPU 后台任务只做计算,不直接操作 QWidget,不直接修改 `edit_state`,不直接操作 `front_color`。 + +## 状态缓冲 + +每个图使用两个状态缓冲: + +```text +edit_state +render_state +``` + +### edit_state + +`edit_state` 是 asio 调度线程持有的最新状态。 + +Qt 主线程收到事件后只投递修改命令,不直接写 `edit_state`。 + +所有会影响最终图像的状态变化,都必须在 asio 调度线程中修改 `edit_state`,并递增 `edit_state.version`。 + +典型状态变化包括: + +```text +用户操作 +数据输入 +缩放 +平移 +尺寸变化 +颜色表变化 +显示开关变化 +采样参数变化 +坐标范围变化 +``` + +### render_state + +`render_state` 是后台渲染任务使用的稳定状态快照。 + +提交后台任务时,由 `edit_state` 复制、交换或快照生成 `render_state`。 + +提交时必须保证: + +```text +render_state.version = edit_state.version +``` + +后台 CPU 任务只读取 `render_state` 对应的稳定快照。 + +## 状态版本号 + +两个状态缓冲都必须带 `version`。 + +示例结构: + +```cpp +struct State_Buffer { + std::uint64_t version = 0; + Chart_State state; +}; +``` + +`edit_state.version` 是整个渲染管线的源版本。 + +`render_state.version` 表示当前提交给后台计算的状态版本。 + +状态版本流向: + +```text +edit_state.version + ↓ submit / snapshot +render_state.version +``` + +判断是否需要提交后台任务: + +```text +edit_state.version 新于 render_state.version +并且当前图没有后台任务在飞 +``` + +## 颜色缓冲 + +每个图使用三个颜色缓冲: + +```text +front_color +ready_color +render_color +``` + +### front_color + +`front_color` 是 Qt 主线程当前正在显示的颜色图。 + +Qt 主线程在 `paintEvent` 中绘制 `front_color`。 + +后台线程和 asio 调度线程不能写 `front_color` 的像素内容。 + +后台发布流程不能直接交换或修改 `front_color`。 + +### ready_color + +`ready_color` 是后台已经生成完成、等待 Qt 主线程消费的颜色图。 + +`ready_color` 是后台发布线程和 Qt 主线程之间的唯一交接槽。 + +如果旧的 `ready_color` 还没被 Qt 主线程消费,而新的 `render_color` 又完成了,直接用新的 `render_color` 覆盖旧的 +`ready_color`。 + +图表库追最新状态,不做帧队列。 + +### render_color + +`render_color` 是后台线程正在写入的颜色图。 + +Qt 主线程不能读 `render_color`。 + +Qt 主线程不能交换 `render_color`。 + +后台 CPU 任务只写 `render_color`。 + +## 颜色版本号 + +每个颜色缓冲对象都带 `version`。 + +示例结构: + +```cpp +struct Color_Buffer { + std::uint64_t version = 0; + QImage image; +}; +``` + +颜色缓冲的 `version` 表示: + +```text +这张颜色图对应哪个 state 版本 +``` + +后台 CPU 渲染完成后,必须设置: + +```text +render_color.version = render_state.version +``` + +颜色版本不是独立递增出来的,而是从状态版本传递过来的。 + +完整版本流向: + +```text +edit_state.version + ↓ submit / snapshot +render_state.version + ↓ CPU render +render_color.version + ↓ publish +ready_color.version + ↓ paintEvent consume +front_color.version +``` + +## 版本号原则 + +每个缓冲对象都带 `version`。 + +版本号跟随缓冲对象,不单独跟随槽位。 + +版本号属于缓冲对象,不属于 `front`、`ready`、`render` 这些槽位名称。 + +也就是说,交换的是缓冲对象指针,版本号跟着缓冲对象一起移动。 + +不要让版本号和图像对象分离。 + +## 回环版本比较 + +版本比较必须封装成支持回环的函数,不要到处直接写 `>`。 + +函数语义: + +```text +version_newer(a, b) +``` + +表示: + +```text +a 是否新于 b +``` + +可以使用 `uint64_t`,并通过有符号差值处理回环。 + +前提是两个待比较版本之间的距离不会超过半个取值空间。 + +示例语义: + +```cpp +static bool version_newer(std::uint64_t a, std::uint64_t b) { + return static_cast(a - b) > 0; +} +``` + +所有版本新旧判断都必须通过该函数完成。 + +## 颜色缓冲分配原则 + +颜色缓冲对象固定分配。 + +槽位只交换指针。 + +不要在发布流程里反复分配或释放颜色缓冲对象。 + +推荐模型: + +```text +front_color 指针 +ready_color 指针 +render_color 指针 +``` + +三个槽位分别指向三个固定分配的 `Color_Buffer` 对象。 + +交换槽位时只交换指针,不拷贝 QImage,不拷贝像素数组。 + +## 颜色缓冲锁原则 + +所有跨线程槽位交换必须加锁。 + +锁内只允许做: + +```text +版本判断 +指针交换 +状态标记 +读取 dirty rect +设置必要标志位 +``` + +锁内禁止做: + +```text +像素计算 +内存分配 +QImage 填充 +drawImage +复杂状态计算 +后台等待 +任务提交等待 +``` + +原则: + +```text +大计算锁外做。 +锁内只换身份。 +``` + +## 颜色缓冲唯一合法交换 + +整个系统只允许两个颜色缓冲交换操作。 + +后台发布: + +```text +ready_color <-> render_color +``` + +Qt 主线程消费: + +```text +front_color <-> ready_color +``` + +不允许: + +```text +front_color <-> render_color +后台线程操作 front_color +Qt 主线程操作 render_color +绕过 ready_color 直接发布 +``` + +## 后台发布流程 + +后台 CPU 任务只写 `render_color`。 + +`render_color` 填充完成后,完成事件回到 asio 调度线程。 + +asio 调度线程在 `color_lock` 下执行发布。 + +发布逻辑: + +```text +如果 render_color.version 新于 ready_color.version: + 交换 ready_color 和 render_color +``` + +交换后判断: + +```text +如果 ready_color.version 新于 front_color.version: + scheduler->request_update() +``` + +如果旧 `ready_color` 还没被 Qt 主线程消费,而新 `render_color` 又完成了: + +```text +直接用新的 render_color 覆盖旧 ready_color。 +旧 ready_color 变成新的 render_color,下一轮会被后台覆盖。 +``` + +图表库只保留最新待显示图,不做帧队列。 + +## Qt paintEvent 流程 + +`paintEvent` 中在 `color_lock` 下判断: + +```text +如果 ready_color.version 新于 front_color.version: + 交换 ready_color 和 front_color +``` + +释放锁后执行: + +```text +drawImage(front_color) +``` + +`paintEvent` 不允许做: + +```text +状态计算 +颜色映射 +后台等待 +任务提交 +像素生成 +大数组遍历 +``` + +`paintEvent` 只负责消费最新 `ready_color` 并绘制 `front_color`。 + +即使没有新 `ready_color`,`paintEvent` 也必须能够直接绘制旧的 `front_color`。 + +因为 Qt 可能因为窗口遮挡、恢复、缩放、系统重绘等原因触发 `paintEvent`。 + +## 刷新请求流程 + +后台发布 `ready_color` 后,不直接在后台线程操作 QWidget。 + +`scheduler->request_update()` 负责把刷新请求投递到 Qt GUI 线程。 + +刷新请求需要使用 `paint_request_pending` 合并重复 update 请求。 + +多个图完成时合并 dirty region。 + +不要每个图都直接 update 整个窗口。 + +Qt 主线程收到刷新任务后调用: + +```text +QWidget::update(region) +``` + +由 Qt 稍后触发 `paintEvent`。 + +## paint_request_pending 语义 + +`paint_request_pending` 表示: + +```text +已经有一个刷新请求投递到 Qt GUI 线程,暂时不要重复投递 +``` + +判断是否投递 update: + +```text +ready_color.version 新于 front_color.version +并且 paint_request_pending == false +``` + +`paint_request_pending` 只用于合并 Qt update 请求,不用于判断颜色图是否新。 + +颜色图是否新仍然通过: + +```text +version_newer(ready_color.version, front_color.version) +``` + +判断。 + +## 任务提交原则 + +每个图同一时间最多一个后台渲染任务。 + +如果任务正在 `Queued` 或 `Running`,不再提交同一图的新任务。 + +任务运行期间 `edit_state` 又变化,只记录新版本。 + +当前任务完成后,下一轮直接提交最新 `edit_state`。 + +不要排队计算中间版本。 + +图不可见、尺寸无效、版本没变时,不提交任务。 + +## 任务状态 + +每个图至少需要表达以下任务状态: + +```text +Idle +Queued +Running +``` + +状态语义: + +```text +Idle: + 当前图没有后台任务。 + +Queued: + 当前图的后台任务已经提交,但还没开始执行。 + +Running: + 当前图的后台任务正在执行。 +``` + +提交条件: + +```text +job_state == Idle +edit_state.version 新于 render_state.version +图可见 +尺寸有效 +没有超过提交频率限制 +``` + +## 状态缓冲提交流程 + +asio 调度线程判断: + +```text +edit_state.version 是否新于 render_state.version +``` + +如果需要提交,并且当前图没有任务在飞: + +```text +生成 render_state 快照 +render_state.version = edit_state.version +标记任务运行 +把 render_state 对应的计算任务提交到后台 CPU 执行环境 +``` + +CPU 任务只读取 `render_state` 快照并写 `render_color`。 + +CPU 任务不直接修改 `edit_state`。 + +CPU 任务不直接发布 `ready_color`。 + +CPU 任务完成后 post 回 asio 调度线程,由 asio 调度线程执行颜色缓冲发布。 + +## 过期任务处理原则 + +如果后台任务计算的是较旧版本,例如: + +```text +任务开始时 render_state.version = 100 +任务运行期间 edit_state.version 已经变成 110 +``` + +默认策略: + +```text +允许 version 100 的结果发布 +任务完成后下一轮直接提交最新 edit_state.version = 110 +不排队计算 101 到 109 +``` + +这样界面不会长时间完全没有更新,同时也不会浪费 CPU 计算中间版本。 + +如果某些图要求严格不显示过期结果,可以单独设计策略,但默认通用框架采用“允许当前任务结果发布,下一轮追最新”的方式。 + +## render_color 写入原则 + +后台 CPU 任务写 `render_color` 前,必须保证 `render_color` 当前不属于 `front_color` 或 `ready_color`。 + +这个由三缓冲槽位交换规则保证。 + +后台 CPU 任务只能写当前槽位名为 `render_color` 的缓冲对象。 + +不要在未持有正确所有权的情况下写任意颜色缓冲。 + +如果使用 `QImage`,要注意 Qt 隐式共享。 + +每个颜色缓冲应持有独立的 QImage 存储。 + +后台写像素前,需要保证 `render_color.image` 独占可写。 + +不要让多个颜色缓冲共享同一块 QImage data。 + +## 锁和绘制的关系 + +锁保护的是: + +```text +front_color / ready_color / render_color 三个槽位身份 +版本号和图像对象的一致性 +ready_color 这个共享交接区 +``` + +锁不保护: + +```text +后台像素写入过程 +主线程 drawImage 的耗时过程 +CPU 计算过程 +状态计算过程 +``` + +正确方式: + +```text +后台: + 锁外写 render_color.image + 锁内 ready_color <-> render_color + +Qt GUI: + 锁内 ready_color <-> front_color + 锁外 drawImage(front_color.image) +``` + +## 通用缓冲控制抽象 + +需要重构出一个通用缓冲控制抽象。 + +该抽象负责: + +```text +状态版本管理 +状态快照提交 +任务 single-flight 控制 +颜色三缓冲管理 +版本回环比较 +颜色发布 +Qt update 请求合并 +dirty region 合并 +``` + +具体图只实现: + +```text +State -> Color 的 CPU 渲染逻辑 +``` + +不要把线程控制、版本控制、颜色缓冲交换散落到每个具体图实现里。 + +## 最终模型总结 + +每个图拥有: + +```text +edit_state +render_state + +front_color +ready_color +render_color +``` + +版本流向: + +```text +edit_state.version + ↓ +render_state.version + ↓ +render_color.version + ↓ +ready_color.version + ↓ +front_color.version +``` + +线程职责: + +```text +Qt GUI 线程: + 接收用户事件 + 投递状态修改命令 + 执行 QWidget::update(region) + paintEvent 中消费 ready_color + drawImage(front_color) + +asio 调度线程: + 串行修改 edit_state + 递增 edit_state.version + 判断是否提交后台任务 + 生成 render_state 快照 + 接收后台任务完成事件 + 发布 ready_color + 合并刷新请求 + +后台 CPU 执行环境: + 读取 render_state + 写 render_color + 完成后 post 回 asio 调度线程 +``` + +核心原则: + +```text +state version 驱动计算。 +color version 追踪产物。 +single-flight 避免堆任务。 +三颜色缓冲保证图像安全交接。 +ready_color 只保存最新待显示图。 +paintEvent 只贴 front_color。 +锁内只交换指针。 +大计算全部锁外完成。 +```