协程架构重构成功, 下一步准备指针交换 无锁化重构

This commit is contained in:
2026-07-23 08:51:06 +08:00
parent eb0bae4ab9
commit 11c77dedd3
+674 -33
View File
@@ -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<std::int64_t>(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。
锁内只交换指针。
大计算全部锁外完成。
```