常规更新
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 415 KiB |
-689
@@ -1,689 +0,0 @@
|
||||
# 多线程图表库后台渲染缓冲模型重构说明
|
||||
|
||||
## 基本要求
|
||||
|
||||
我要重构这个多线程图表库的后台渲染缓冲模型。
|
||||
|
||||
要求:
|
||||
|
||||
* 不要动 git。
|
||||
* 代码保持 K&R 样式。
|
||||
* 不要加无意义空行。
|
||||
* 除非必要,不要破坏已有函数签名语义。
|
||||
* 如果替换实现,旧实现直接删除,不要做兼容实现。
|
||||
* 能不做兼容实现就不做兼容实现,除非明确说明设计如此。
|
||||
|
||||
## 核心目标
|
||||
|
||||
把每个图抽象成下面这个通用模型:
|
||||
|
||||
```text
|
||||
状态缓冲 -> 后台 CPU 渲染 -> 颜色缓冲 -> Qt 主线程贴图
|
||||
```
|
||||
|
||||
也就是:
|
||||
|
||||
```text
|
||||
State -> Color -> paintEvent drawImage
|
||||
```
|
||||
|
||||
具体图只实现:
|
||||
|
||||
```text
|
||||
State -> Color 的 CPU 渲染逻辑
|
||||
```
|
||||
|
||||
线程调度、版本判断、缓冲交换、刷新合并逻辑放到通用框架里。
|
||||
|
||||
## 线程模型
|
||||
|
||||
Qt GUI 线程和 asio 调度线程是两个不同线程。
|
||||
|
||||
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。
|
||||
锁内只交换指针。
|
||||
大计算全部锁外完成。
|
||||
```
|
||||
+727
-41
@@ -1,53 +1,739 @@
|
||||
每一个图有自己的元数据
|
||||
# 图表渲染整体架构设计
|
||||
|
||||
单独看流程就是 元数据 -> cpu计算 然后出来一个装满像素的数组 然后主线程合适时机贴图
|
||||
## 1. 总体流程
|
||||
|
||||
其实真正流程就是主线程用户的各种操作刷新元数据缓冲(无锁操作)。 协程定时器(也通过版本号来判断是否计算还是等待下一个周期)合适的时机触发(加锁交换缓冲)扔给cpu线程池去计算出图
|
||||
然后加锁交换缓冲 标记版本号 主线程定时器看到版本号决定是否paint event
|
||||
每一个图都有自己的元数据、状态缓冲和颜色缓冲。
|
||||
|
||||
如果这个流程的话 如果
|
||||
|
||||

|
||||
|
||||

|
||||
单独看一个图,核心流程是:
|
||||
|
||||
```text
|
||||
元数据 / 状态
|
||||
↓
|
||||
CPU 后台计算
|
||||
↓
|
||||
像素数组 / QImage
|
||||
↓
|
||||
Qt 主线程在合适时机贴图
|
||||
```
|
||||
使用asio做协程调度器,单线程调度线程 调度所有执行流程。状态缓冲计算到颜色缓冲,放到后台asio协程池里
|
||||
封装成一个awitable,也由单线程调度线程 调度。
|
||||
注意 Qt 主线程 和 asio 调度线程 是两个不同线程。
|
||||
|
||||
完整流程是:
|
||||
|
||||
抽象缓冲控制
|
||||
版本号 (需要回环) 每个缓冲都带版本号
|
||||
```text
|
||||
用户操作 / 数据输入 / 缩放 / 尺寸变化 / 颜色表变化
|
||||
↓
|
||||
更新 edit_state
|
||||
↓
|
||||
edit_state 快照到 ready_state
|
||||
↓
|
||||
调度线程在合适周期提交 ready_state
|
||||
↓
|
||||
ready_state 切换成 render_state
|
||||
↓
|
||||
CPU 线程池读取 render_state 计算颜色图
|
||||
↓
|
||||
写入 render_color
|
||||
↓
|
||||
render_color 发布成 ready_color
|
||||
↓
|
||||
Qt 主线程消费 ready_color
|
||||
↓
|
||||
front_color 贴图显示
|
||||
```
|
||||
|
||||
两个状态缓冲:
|
||||
edit_state 主线程正在接收用户操作、数据输入、缩放、颜色表、尺寸变化等修改的状态。
|
||||
render_state 后台线程正在使用的稳定状态快照。
|
||||
也就是:
|
||||
|
||||
三个颜色缓冲:
|
||||
front_color 主线程当前正在显示的颜色图。
|
||||
ready_color 后台已经生成完,等待主线程消费的颜色图。
|
||||
render_color 后台线程正在写入的颜色图。
|
||||
```text
|
||||
edit_state
|
||||
↓ snapshot
|
||||
ready_state
|
||||
↓ swap ready/render
|
||||
render_state
|
||||
↓ CPU render
|
||||
render_color
|
||||
↓ swap render/ready
|
||||
ready_color
|
||||
↓ swap ready/front
|
||||
front_color
|
||||
↓ drawImage
|
||||
```
|
||||
|
||||
后台发布:
|
||||
render_color -> ready_color
|
||||
如果 ready_color.version > front_color.version
|
||||
---
|
||||
|
||||
## 2. 线程模型
|
||||
|
||||
使用 asio 做协程调度器。
|
||||
|
||||
有三个主要执行环境:
|
||||
|
||||
```text
|
||||
Qt 主线程
|
||||
asio 单线程调度线程
|
||||
CPU 后台线程池
|
||||
```
|
||||
|
||||
### Qt 主线程
|
||||
|
||||
负责:
|
||||
|
||||
```text
|
||||
接收用户输入
|
||||
接收鼠标、键盘、resize、paintEvent
|
||||
把用户操作投递给 asio 调度线程
|
||||
执行 QWidget::update()
|
||||
在 paintEvent 中 drawImage(front_color)
|
||||
```
|
||||
|
||||
Qt 主线程不直接做重计算。
|
||||
|
||||
Qt 主线程也不直接修改后台正在使用的状态缓冲。
|
||||
|
||||
推荐语义是:
|
||||
|
||||
```text
|
||||
Qt 主线程只产生操作命令。
|
||||
asio 调度线程把操作命令应用到 edit_state。
|
||||
```
|
||||
|
||||
这样 edit_state 的“无锁修改”来自于单线程所有权,而不是多个线程裸写同一份状态。
|
||||
|
||||
### asio 单线程调度线程
|
||||
|
||||
负责:
|
||||
|
||||
```text
|
||||
串行处理用户操作命令
|
||||
修改 edit_state
|
||||
递增 edit_state.version
|
||||
执行 edit_state -> ready_state 快照
|
||||
通过协程定时器判断是否需要提交后台计算
|
||||
把 ready_state 切换成 render_state
|
||||
把后台计算任务提交给 CPU 线程池
|
||||
接收 CPU 任务完成事件
|
||||
发布 render_color -> ready_color
|
||||
合并 request_update
|
||||
```
|
||||
|
||||
asio 调度线程是整个图表流水线的控制中心。
|
||||
|
||||
### CPU 后台线程池
|
||||
|
||||
负责:
|
||||
|
||||
```text
|
||||
读取 render_state
|
||||
计算颜色图
|
||||
写入 render_color
|
||||
设置 render_color.version
|
||||
完成后 post 回 asio 调度线程
|
||||
```
|
||||
|
||||
CPU 线程池不操作 QWidget。
|
||||
|
||||
CPU 线程池不修改 edit_state。
|
||||
|
||||
CPU 线程池不直接触发 paintEvent。
|
||||
|
||||
---
|
||||
|
||||
## 3. 抽象缓冲控制器
|
||||
|
||||
状态缓冲和颜色缓冲都使用同一类抽象:
|
||||
|
||||
```text
|
||||
固定缓冲池
|
||||
+
|
||||
角色状态 slot_state
|
||||
+
|
||||
物理占用标记 busy
|
||||
+
|
||||
版本号 version
|
||||
```
|
||||
|
||||
每个缓冲对象固定分配,不在运行过程中反复 new/delete。
|
||||
|
||||
角色只是映射关系。
|
||||
|
||||
物理缓冲才是真正被读写的对象。
|
||||
|
||||
### 核心概念
|
||||
|
||||
```text
|
||||
slot_state:
|
||||
表示角色到物理 buffer 的映射。
|
||||
|
||||
busy:
|
||||
表示某个物理 buffer 当前正在被长时间使用。
|
||||
|
||||
version:
|
||||
表示这个物理 buffer 当前内容对应的版本。
|
||||
|
||||
lease:
|
||||
表示一次具体的物理 buffer 占用。
|
||||
```
|
||||
|
||||
### 核心操作
|
||||
|
||||
抽象接口只需要三个基础操作:
|
||||
|
||||
```text
|
||||
mark_use
|
||||
unmark_use
|
||||
swap_role
|
||||
```
|
||||
|
||||
语义:
|
||||
|
||||
```text
|
||||
mark_use:
|
||||
标记某个物理 buffer 正在被使用。
|
||||
成功后返回 lease。
|
||||
|
||||
unmark_use:
|
||||
释放 lease 对应的物理 buffer。
|
||||
|
||||
swap_role:
|
||||
交换两个角色对应的物理 buffer。
|
||||
交换前检查参与交换的物理 buffer 是否被占用。
|
||||
支持 Try 和 Wait 两种模式。
|
||||
```
|
||||
|
||||
### swap_role 结果
|
||||
|
||||
交换操作必须返回明确结果:
|
||||
|
||||
```text
|
||||
Success:
|
||||
交换成功。
|
||||
|
||||
Busy:
|
||||
参与交换的 buffer 正在被占用。
|
||||
|
||||
State_Changed:
|
||||
操作过程中 slot_state 已经变化,需要重试。
|
||||
|
||||
Condition_Failed:
|
||||
业务条件不满足,例如版本没有更新。
|
||||
```
|
||||
|
||||
### swap_role 模式
|
||||
|
||||
```text
|
||||
Try:
|
||||
尝试一次。
|
||||
如果 buffer 被占用,立即返回 Busy。
|
||||
适合 Qt paintEvent 等不能阻塞的路径。
|
||||
|
||||
Wait:
|
||||
如果 buffer 被占用,就等待释放后重试。
|
||||
适合 asio 调度线程或后台发布路径。
|
||||
不允许在 Qt paintEvent 里使用。
|
||||
```
|
||||
|
||||
### 交换规则
|
||||
|
||||
交换不是简单改指针,而是:
|
||||
|
||||
```text
|
||||
读取 slot_state
|
||||
找到两个角色对应的物理 buffer
|
||||
尝试 mark_use 这两个物理 buffer
|
||||
mark 成功后重新读取 slot_state
|
||||
确认角色映射没有变化
|
||||
检查业务条件
|
||||
CAS 提交新的 slot_state
|
||||
释放两个物理 buffer
|
||||
```
|
||||
|
||||
核心原则:
|
||||
|
||||
```text
|
||||
slot_state 管角色。
|
||||
busy 管物理占用。
|
||||
version 管内容新旧。
|
||||
lease 管一次具体使用。
|
||||
```
|
||||
|
||||
不能混用。
|
||||
|
||||
不能用 version 判断 buffer 是否正在被使用。
|
||||
|
||||
不能只用 slot_state 判断 buffer 是否安全可写。
|
||||
|
||||
不能释放 role,必须释放 lease。
|
||||
|
||||
因为 role 在使用期间可能已经被交换,lease 才代表真正占用的物理 buffer。
|
||||
|
||||
---
|
||||
|
||||
## 4. 版本号规则
|
||||
|
||||
每个状态缓冲和颜色缓冲都带 version。
|
||||
|
||||
版本号使用 uint64_t,并支持回环比较。
|
||||
|
||||
```cpp
|
||||
static bool version_newer(std::uint64_t left, std::uint64_t right) {
|
||||
return static_cast<std::int64_t>(left - right) > 0;
|
||||
}
|
||||
```
|
||||
|
||||
语义:
|
||||
|
||||
```text
|
||||
version_newer(left, right)
|
||||
表示 left 是否新于 right。
|
||||
```
|
||||
|
||||
版本号跟随物理缓冲对象,不跟随角色槽位。
|
||||
|
||||
角色切换时,不单独修改版本号。
|
||||
|
||||
版本号只在内容真正更新后修改。
|
||||
|
||||
---
|
||||
|
||||
## 5. 状态三缓冲
|
||||
|
||||
状态缓冲也使用三缓冲。
|
||||
|
||||
每个图有三个状态缓冲:
|
||||
|
||||
```text
|
||||
states[3]
|
||||
```
|
||||
|
||||
三个角色:
|
||||
|
||||
```text
|
||||
edit_state
|
||||
ready_state
|
||||
render_state
|
||||
```
|
||||
|
||||
### edit_state
|
||||
|
||||
```text
|
||||
asio 调度线程正在修改的最新状态。
|
||||
```
|
||||
|
||||
用户操作、数据输入、缩放、颜色表、尺寸变化等最终都作用到 edit_state。
|
||||
|
||||
每次影响最终图像的修改,都递增:
|
||||
|
||||
```text
|
||||
edit_state.version
|
||||
```
|
||||
|
||||
### ready_state
|
||||
|
||||
```text
|
||||
已经从 edit_state 快照出来,等待提交给 CPU 的状态。
|
||||
```
|
||||
|
||||
ready_state 是交接缓冲。
|
||||
|
||||
它可以在 CPU 正在读取 render_state 时,被新的 edit_state 覆盖。
|
||||
|
||||
这样可以跳过中间状态,只保留最新待计算状态。
|
||||
|
||||
### render_state
|
||||
|
||||
```text
|
||||
CPU 后台任务正在读取的稳定状态。
|
||||
```
|
||||
|
||||
render_state 被 CPU 读取期间必须 mark_use。
|
||||
|
||||
render_state 被占用时,不能被交换到会被写入的位置。
|
||||
|
||||
### 状态快照流程
|
||||
|
||||
当 edit_state.version 新于 ready_state.version 时:
|
||||
|
||||
```text
|
||||
mark_use(ready_state)
|
||||
copy edit_state -> ready_state
|
||||
ready_state.version = edit_state.version
|
||||
unmark_use(ready_state)
|
||||
```
|
||||
|
||||
如果 edit_state 只由 asio 调度线程修改,edit_state 本身可以不显式 mark_use。
|
||||
|
||||
如果以后允许其他线程直接写 edit_state,则 edit_state 也必须 mark_use。
|
||||
|
||||
### 状态提交流程
|
||||
|
||||
协程定时器在合适时机检查:
|
||||
|
||||
```text
|
||||
ready_state.version 是否新于 render_state.version
|
||||
CPU 任务是否空闲
|
||||
render_state 是否未被占用
|
||||
```
|
||||
|
||||
满足条件后:
|
||||
|
||||
```text
|
||||
swap_role(ready_state, render_state, Wait)
|
||||
mark_use(render_state)
|
||||
提交 CPU 后台计算任务
|
||||
```
|
||||
|
||||
CPU 任务完成后:
|
||||
|
||||
```text
|
||||
unmark_use(render_state)
|
||||
```
|
||||
|
||||
状态三缓冲的核心意义是:
|
||||
|
||||
```text
|
||||
CPU 正在读 render_state。
|
||||
asio 继续改 edit_state。
|
||||
ready_state 保存下一次要提交的最新快照。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 颜色三缓冲
|
||||
|
||||
每个图有三个颜色缓冲:
|
||||
|
||||
```text
|
||||
colors[3]
|
||||
```
|
||||
|
||||
三个角色:
|
||||
|
||||
```text
|
||||
front_color
|
||||
ready_color
|
||||
render_color
|
||||
```
|
||||
|
||||
### front_color
|
||||
|
||||
```text
|
||||
Qt 主线程当前正在显示的颜色图。
|
||||
```
|
||||
|
||||
paintEvent 只画 front_color。
|
||||
|
||||
### ready_color
|
||||
|
||||
```text
|
||||
后台已经生成完成,等待 Qt 主线程消费的颜色图。
|
||||
```
|
||||
|
||||
ready_color 是后台发布和 GUI 消费之间的交接缓冲。
|
||||
|
||||
### render_color
|
||||
|
||||
```text
|
||||
CPU 后台线程正在写入的颜色图。
|
||||
```
|
||||
|
||||
CPU 写 render_color 时必须 mark_use。
|
||||
|
||||
写完后设置:
|
||||
|
||||
```text
|
||||
render_color.version = render_state.version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. CPU 计算流程
|
||||
|
||||
asio 调度线程提交任务前,先把 ready_state 切换成 render_state。
|
||||
|
||||
然后 CPU 任务执行:
|
||||
|
||||
```text
|
||||
mark_use(render_state)
|
||||
mark_use(render_color)
|
||||
读取 render_state
|
||||
计算像素
|
||||
写入 render_color
|
||||
render_color.version = render_state.version
|
||||
unmark_use(render_color)
|
||||
unmark_use(render_state)
|
||||
post 完成事件到 asio 调度线程
|
||||
```
|
||||
|
||||
实际实现中,如果 mark_use (render_state) 已经在提交任务前完成,也可以把 lease 一起传给 CPU 任务,任务结束时释放 lease。
|
||||
|
||||
重点是:
|
||||
|
||||
```text
|
||||
CPU 读取的 render_state 在任务期间不能被复用。
|
||||
CPU 写入的 render_color 在任务期间不能被发布。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 颜色发布流程
|
||||
|
||||
CPU 计算完成后,asio 调度线程发布颜色图。
|
||||
|
||||
发布逻辑:
|
||||
|
||||
```text
|
||||
如果 render_color.version 新于 ready_color.version:
|
||||
swap_role(ready_color, render_color, Try 或 Wait)
|
||||
```
|
||||
|
||||
发布成功后:
|
||||
|
||||
```text
|
||||
如果 ready_color.version 新于 front_color.version:
|
||||
scheduler->request_update()
|
||||
|
||||
调度器:
|
||||
用 paint_request_pending 合并 update 请求
|
||||
|
||||
paintEvent:
|
||||
如果 ready_color.version > front_color.version
|
||||
ready_color <-> front_color
|
||||
drawImage(front_color)
|
||||
|
||||
|
||||
缓冲对象固定分配,槽位只交换指针。
|
||||
版本号跟随缓冲对象,不单独跟随槽位。
|
||||
所有跨线程槽位交换必须加锁。
|
||||
锁内只允许做版本判断、指针交换、状态标记。
|
||||
锁内禁止做像素计算、内存分配、QImage 填充、drawImage。
|
||||
大活锁外做。
|
||||
锁内只换身份。
|
||||
```
|
||||
|
||||
这里的 ready_color 是交换后的新 ready_color。
|
||||
|
||||
request_update 不直接绘制,只是向 Qt 主线程请求下一次 update。
|
||||
|
||||
---
|
||||
|
||||
## 9. Qt 更新合并
|
||||
|
||||
调度器使用 paint_request_pending 合并 update 请求。
|
||||
|
||||
语义:
|
||||
|
||||
```text
|
||||
paint_request_pending 表示已经有一个 update 请求在路上。
|
||||
```
|
||||
|
||||
它不表示是否有新图。
|
||||
|
||||
是否有新图仍然通过版本号判断:
|
||||
|
||||
```text
|
||||
ready_color.version 是否新于 front_color.version
|
||||
```
|
||||
|
||||
request_update 流程:
|
||||
|
||||
```text
|
||||
如果 paint_request_pending == false:
|
||||
paint_request_pending = true
|
||||
投递 QWidget::update() 到 Qt 主线程
|
||||
```
|
||||
|
||||
paintEvent 执行后:
|
||||
|
||||
```text
|
||||
paint_request_pending = false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. paintEvent 流程
|
||||
|
||||
paintEvent 不能等待。
|
||||
|
||||
paintEvent 只能使用 Try 模式。
|
||||
|
||||
流程:
|
||||
|
||||
```text
|
||||
尝试 swap_role(front_color, ready_color, Try)
|
||||
如果成功:
|
||||
新 ready_color 被消费成 front_color
|
||||
如果失败:
|
||||
继续使用旧 front_color
|
||||
mark_use(front_color)
|
||||
drawImage(front_color)
|
||||
unmark_use(front_color)
|
||||
```
|
||||
|
||||
paintEvent 的核心规则:
|
||||
|
||||
```text
|
||||
不等待后台。
|
||||
不做 CPU 计算。
|
||||
不分配大内存。
|
||||
不填充 QImage。
|
||||
只消费 ready_color。
|
||||
只绘制 front_color。
|
||||
```
|
||||
|
||||
如果 Qt paintEvent 保证单线程不重入,并且后台永远不写 front_color,front_color 的 mark_use 可以作为优化项省略。
|
||||
|
||||
但从统一抽象上看,front_color 在 drawImage 期间属于被长时间读取的缓冲,最好仍然按 lease 模型处理。
|
||||
|
||||
---
|
||||
|
||||
## 11. 协程定时器调度
|
||||
|
||||
每个图可以由 asio 协程定时器驱动调度。
|
||||
|
||||
定时器周期到来时,不一定立刻计算。
|
||||
|
||||
它先判断版本:
|
||||
|
||||
```text
|
||||
edit_state.version 是否新于 ready_state.version
|
||||
ready_state.version 是否新于 render_state.version
|
||||
当前 CPU 任务是否空闲
|
||||
当前图是否可见
|
||||
当前尺寸是否有效
|
||||
是否超过刷新频率限制
|
||||
```
|
||||
|
||||
典型流程:
|
||||
|
||||
```text
|
||||
如果 edit_state 新于 ready_state:
|
||||
快照 edit_state -> ready_state
|
||||
|
||||
如果 ready_state 新于 render_state 且 CPU 任务空闲:
|
||||
ready_state <-> render_state
|
||||
提交 CPU 计算任务
|
||||
|
||||
否则:
|
||||
等待下一个周期
|
||||
```
|
||||
|
||||
这个机制天然跳过中间状态。
|
||||
|
||||
例如:
|
||||
|
||||
```text
|
||||
CPU 正在计算 version 100
|
||||
用户连续操作到 version 110
|
||||
ready_state 最终只保留 version 110
|
||||
CPU 完成 100 后,下一轮直接计算 110
|
||||
```
|
||||
|
||||
图表库追求最新状态,不排旧帧队列。
|
||||
|
||||
---
|
||||
|
||||
## 12. single-flight
|
||||
|
||||
每个图同一时间只允许一个 CPU 计算任务。
|
||||
|
||||
任务状态可以是:
|
||||
|
||||
```text
|
||||
Idle
|
||||
Queued
|
||||
Running
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
```text
|
||||
只有 Idle 才能提交新的 CPU 任务。
|
||||
Running 期间继续更新 edit_state 和 ready_state。
|
||||
Running 完成后,如果 ready_state 新于 render_state,再提交下一轮。
|
||||
```
|
||||
|
||||
single-flight 的作用是:
|
||||
|
||||
```text
|
||||
防止多个 CPU 任务同时读取/写入同一组缓冲。
|
||||
防止旧版本任务并发污染新版本结果。
|
||||
避免无意义的中间帧堆积。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. 状态和颜色的完整版本链路
|
||||
|
||||
版本链路是:
|
||||
|
||||
```text
|
||||
edit_state.version
|
||||
↓ snapshot
|
||||
ready_state.version
|
||||
↓ swap ready/render
|
||||
render_state.version
|
||||
↓ CPU render
|
||||
render_color.version
|
||||
↓ swap render/ready
|
||||
ready_color.version
|
||||
↓ swap ready/front
|
||||
front_color.version
|
||||
```
|
||||
|
||||
颜色版本不是独立生成的。
|
||||
|
||||
颜色版本来自 render_state.version。
|
||||
|
||||
也就是:
|
||||
|
||||
```text
|
||||
render_color.version = render_state.version
|
||||
```
|
||||
|
||||
这表示这张颜色图是由哪个状态版本计算出来的。
|
||||
|
||||
---
|
||||
|
||||
## 14. 锁和无锁边界
|
||||
|
||||
旧设计里的“加锁交换缓冲”可以替换成:
|
||||
|
||||
```text
|
||||
缓冲控制器交换角色
|
||||
```
|
||||
|
||||
缓冲控制器内部可以使用:
|
||||
|
||||
```text
|
||||
atomic busy
|
||||
atomic slot_state
|
||||
CAS
|
||||
atomic_wait
|
||||
```
|
||||
|
||||
Try 路径可以不使用 mutex。
|
||||
|
||||
Wait 路径可以不使用 mutex,但会等待 atomic 状态变化。
|
||||
|
||||
所以更准确的说法是:
|
||||
|
||||
```text
|
||||
不是所有交换都必须用 mutex。
|
||||
交换必须通过缓冲控制器完成。
|
||||
控制器保证占用检查、角色确认和 CAS 交换的原子提交语义。
|
||||
```
|
||||
|
||||
锁内原则可以改成控制器原则:
|
||||
|
||||
```text
|
||||
大活在 lease 持有期间做。
|
||||
角色切换只通过 swap_role 做。
|
||||
swap_role 内部只做占用检查、版本条件判断、slot_state CAS。
|
||||
swap_role 内禁止像素计算、内存分配、QImage 填充、drawImage。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. 最终核心规则
|
||||
|
||||
```text
|
||||
缓冲对象固定分配。
|
||||
角色只通过 slot_state 映射。
|
||||
版本号跟随物理缓冲对象。
|
||||
每个缓冲可以被长时间占用。
|
||||
长时间使用前必须 mark_use。
|
||||
释放时必须 unmark_use lease。
|
||||
角色交换必须通过 swap_role。
|
||||
swap_role 必须返回明确成功或失败。
|
||||
Qt paintEvent 只使用 Try 模式。
|
||||
后台调度可以使用 Wait 模式。
|
||||
CPU 任务 single-flight。
|
||||
状态三缓冲负责输入侧交接。
|
||||
颜色三缓冲负责输出侧交接。
|
||||
```
|
||||
|
||||
最终架构一句话:
|
||||
|
||||
```text
|
||||
每个图使用两组三缓冲:状态三缓冲负责 edit/ready/render 的元数据交接,颜色三缓冲负责 render/ready/front 的像素交接;所有物理缓冲都有版本号和占用标记,所有角色切换都通过缓冲控制器以 CAS 方式提交,长时间读写通过 lease 保护。
|
||||
```
|
||||
@@ -76,7 +76,7 @@ QWidget* IntermediateFrequencyWidget::createSpectrogramPowerPlot() {
|
||||
mProgressBar1->mSetValue = [this]() {
|
||||
double middleFrequent = this->mSpectrogramLine->frequentRange().center();
|
||||
bool ok;
|
||||
this->mProgressBar1->mValue = mSpectrogramLine->getPower(middleFrequent, ok, YSG::SRC::Cache);
|
||||
this->mProgressBar1->mValue = mSpectrogramLine->getPower(middleFrequent, ok, YSG::SRC::Edit);
|
||||
};
|
||||
return w;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ QWidget* RadioWidget::createSpectrogramPowerPlot() {
|
||||
mProgressBar1->mSetValue = [this]() {
|
||||
double middleFrequent = this->mSpectrogramLine->frequentRange().center();
|
||||
bool ok;
|
||||
this->mProgressBar1->mValue = mSpectrogramLine->getPower(middleFrequent, ok, YSG::SRC::Cache);
|
||||
this->mProgressBar1->mValue = mSpectrogramLine->getPower(middleFrequent, ok, YSG::SRC::Edit);
|
||||
};
|
||||
return w;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user