18 KiB
Renderive 线程同步模型
本文只描述线程同步、所有权、锁和并发约束,不提出新的架构改造方案。
当前版本按已经回退到原有 Frame Strategy 语义后的设计理解编写。
文中分为:
- 已确认:已经明确的设计语义。
- 待确认:需要结合最终回退代码或由项目设计者确认的细节。
本文不把
viewport、configuration、render graph重新设计成额外的加锁对象,也不把Render_Lease改成短 claim/CAS。
1. 总体线程模型
Renderive 的线程模型分成四类执行活动:
- Plot / Scene 控制调用。
- Frame Strategy 的帧生产、渲染和发布。
- Scene Render DAG 的 Taskflow 并行执行。
- Real-Time Data(RTD)生产、维护和读取。
在 render_2D 层,一个 Plot_Core 持有一个具体的 Scene2D_Context。因此日常讨论 2D Plot 时,可以把“一个 Scene 的渲染执行域”理解成“一个 Plot 的渲染执行域”。
概念关系:
Plot_Core
└── Scene2D_Context
├── Renderable A
│ └── local DAG
├── Renderable B
│ └── local DAG
├── Renderable C
│ └── local DAG
└── global Render DAG
多个 Renderable 的局部 DAG 组合成当前 Scene / Plot 的全局 Render DAG。
2. Scene / Plot 控制域
2.1 控制域负责的内容
控制域负责修改 Plot / Scene 的结构和配置,例如:
attach / detach Renderable
Renderable 配置修改
Renderable DAG 结构修改请求
viewport 修改
layer / dependency 修改
Frame Strategy 配置修改
2.2 控制域和正在执行的 Render 的关系
已确认:
控制操作不能破坏当前正在执行的 Render DAG 所依赖的数据和结构。
当前已有的 Scene 串行化 / render-idle 机制负责保证部分控制操作不会和不允许并发的 Scene 操作重叠。
这里的同步应该依赖 Scene 原有线程契约,而不是给每一个普通字段都额外套一把 mutex。
2.3 viewport
已确认:
viewport 本身不应该因为 Render DAG 读取而额外加锁。
Render DAG 内不应该通过一个带 mutex 的 Plot_Core::viewport_size() 热路径反复取 viewport。
需要区分:
控制侧修改 viewport
和:
当前 Frame 渲染使用 viewport
具体由现有 Scene / Frame 调用顺序保证一致性。
待确认:
set_viewport_size()是否被明确限定只能由某一控制线程调用。- Frame 开始后是否允许其他线程修改 Plot viewport。
- 当前 Frame 实际使用的 viewport 在哪一个现有对象中固定下来。
本文不擅自为 viewport 新增 mutex、atomic 或新的 publication 结构。
3. Renderable configuration
已确认:
Renderable configuration 不应该在 Render DAG 热路径为了读取配置而加锁。
配置修改属于控制行为。
渲染执行读取配置应该依靠原有 Scene 控制串行化和配置修改规则保证安全,而不是:
Prepare/Paint
↓
configuration()
↓
configuration_mutex_
这种每次执行节点都重新同步的方式。
待确认:
- 配置修改是否全部必须经过
Scene_Base::lock_render_idle()或等价机制。 - 是否存在允许控制线程在 Scene render 期间直接修改 configuration 的公开 API。
- 如果存在,哪些字段允许并发修改,哪些字段必须等待 render idle。
在这些约束确认前,不应新增 per-Renderable configuration mutex 作为兜底。
4. Renderable graph
4.1 Graph 的语义
每个 Renderable 拥有自己的局部 DAG。
局部 DAG 可以:
fan-out
fan-in
一对多
多对一
局部串行
局部并行
多入口
多出口
子图组合
它不是树,只要求整体保持无环。
例如:
┌─ B ─┐
A ──────┼─ C ─┼─ F
└─ D ─┘
也是合法局部 DAG。
4.2 Graph 同步
已确认:
Render DAG 执行阶段读取当前 Renderable graph 不应该再通过 render_graph_mutex_ 做节点级同步。
Graph 结构变化属于控制 / rebuild 行为。
当前 Frame 已经开始执行以后,不应该一边执行同一份 graph,一边原地修改这份 graph。
因此正确线程语义必须是:
旧 graph 正在被当前 Render 使用
||
控制侧请求下一次 graph 结构变化
而不是:
worker 正在遍历 graph
||
另一个线程原地修改同一个 graph 对象
待确认:
rebuild_render_graph()的真正执行线程。- dirty 标记和真正 rebuild 是否分离。
- 当前实现是否已经通过不可变 graph 对象 / shared ownership 保证旧 graph 生命周期。
- graph rebuild 是否只能发生在 Scene 的串行 publication / render-idle 区域。
本文不新增 graph mutex;最终文档应根据回退代码把上述四点写死。
5. Frame Strategy
Frame Strategy 管理的是帧生命周期和帧 ownership,不负责 Render DAG 内部节点的数据同步。
不同 Strategy 可以拥有不同的 Frame 队列 / buffer / publish 规则,例如:
Manual
Flow
Low Latency
其他 Frame Strategy
但 Render DAG 的 Prepare / Paint / Composite 语义不因 Strategy 改变。
6. Render_Lease
6.1 已确认语义
已确认:原有 Render_Lease 生命周期互斥设计保留。
Render_Lease 不是仅用于“哪个 renderer 抢到 Frame”的短 claim。
它的锁可以覆盖:
acquire renderer
↓
scene.render()
↓
Taskflow 执行
↓
wait_for_render()
↓
Render 完成
↓
Render_Lease 结束
如果 Strategy 的 render_mutex_ 同时承担:
当前 Frame 正在 render
与
该 Frame publish / swap
之间的生命周期互斥,那么该锁必须覆盖完整 render 生命周期。
核心不变量:
没有完成渲染的 Frame 不能被发布出去。
因此:
rendering Frame
和:
publish / swap same Frame
必须按当前 Strategy 设计保持互斥。
6.2 这把锁不属于 DAG 内部数据锁
即使 Render_Lease 外层持有 render_mutex_,也不意味着 DAG worker 内部靠这把锁串行执行。
正确关系是:
caller thread
└── 持有 Frame Strategy 生命周期锁
└── 等待 Taskflow
├── worker 0
├── worker 1
├── worker 2
└── worker N
Taskflow workers 仍然根据 DAG dependency 并行执行。
因此必须区分:
Frame Strategy 生命周期锁
和:
Render DAG 数据访问锁
前者允许存在;后者应该尽量不存在于本来无需同步的数据上。
6.3 禁止擅自改成短 claim
除非 Strategy 本身重新设计并明确提供等价的:
render 未完成
↓
禁止 publish
保证,否则不能把完整 Render Lease:
lock → render → wait → unlock
擅自改成:
lock → claim → unlock → render
更不能因为想“无锁”就自行改成 CAS。
7. 三缓冲 / 多缓冲交换
7.1 Frame buffer ownership
Frame Strategy 的 buffer ownership 必须保证:
producer 正在写的 buffer
renderer 正在渲染的 buffer
consumer / publisher 可见的 buffer
不会在不允许的阶段被同一参与者同时修改。
7.2 publish 条件
已确认:
Frame 只有在完成完整 Render DAG 后才能进入可发布状态。
也就是至少:
Prepare
Paint
Composite
需要满足当前 Frame Strategy 所要求的完成条件。
Frame publish / swap 的同步属于 Frame Strategy 层,不应该被“清理 Render DAG 内部锁”的工作改动。
8. Scene Render DAG
8.1 全局 DAG
一个 Scene / Plot 的 Renderables 最终组合为一张全局 DAG。
Renderable 自己拥有局部子 DAG。
全局 DAG 不是严格顺序图,而是 dependency graph。
例如:
A.prepare ─────────→ A.paint ───────┐
│
B.prepare ─→ B.work ─→ B.paint ─────┼→ composite
│
C.prepare ─────────→ C.paint ───────┘
只要 dependency 满足,节点就可以执行。
8.2 没有全局 Prepare barrier
已确认:
不存在:
所有 Prepare 全部完成
↓
所有 Paint 才允许开始
这种全局 barrier。
例如如果 A.paint 只依赖 A.prepare,那么:
A.prepare 完成
以后 A.paint 可以开始,即使:
B.prepare
还没有结束。
8.3 worker 之间的同步
Taskflow worker 之间主要通过 DAG dependency 建立先后关系。
原则:
没有 dependency edge
↓
允许并发
而不是通过一个全局 Render mutex 把所有 Renderable 串起来。
9. Prepare
Prepare 是 Renderable 的数据准备阶段。
它可以执行:
geometry 计算
layout 计算
clip 计算
需要的状态读取
对 Paint 所需数据的准备
局部任务拆分
Prepare 内部也可以是一张 DAG。
9.1 Prepare 的锁
不能简单规定“Prepare 完全不允许锁”。
是否需要锁取决于它访问的数据本身。
例如:
RTD
是动态共享实时数据,本身有同步要求,因此 Prepare 访问 RTD 时允许按 RTD 自身线程模型获取锁。
但是:
viewport
configuration
render graph
不能因为实现方便就在 Prepare 每次读取时新加一层无意义 mutex。
10. Paint
Paint 根据当前 Renderable 已准备好的数据生成像素 / layer / paint output。
10.1 Paint 并发
不同 Renderable 的 Paint 是否能并发,由:
DAG dependency
paint output ownership
composite dependency
决定。
不能依赖一个全局 mutex 控制 Paint 顺序。
10.2 Paint 数据访问
Paint 不应该为了获取普通固定渲染输入反复进入:
Plot mutex
configuration mutex
render graph mutex
10.3 跨 Renderable 数据
已确认的设计方向:
跨 Renderable 的依赖应该显式体现在 Render DAG / Prepare dependency 中,而不是 Paint 临时从其他 Renderable 拉动态数据。
最终允许 Paint 访问哪些 Scene / Renderable API,需要结合回退代码再列出白名单。
11. Composite
Composite 负责按照明确的 layer / z-order / dependency 组合各 Paint 输出。
Paint 的实际线程完成顺序不能代替 layer order。
例如:
A.paint ─┐
B.paint ─┼→ composite
C.paint ─┘
即使 C.paint 最先结束,也不能因此改变最终覆盖顺序。
Composite 顺序是渲染语义,不是 mutex 获得顺序。
12. Real-Time Data(RTD)
RTD = Real-Time Data。
RTD 是持续被生产线程更新、同时可能被渲染线程读取的动态共享数据。
12.1 RTD 与普通配置不同
RTD 不能套用:
viewport/config/graph 不加锁
这一规则。
RTD 本身就是并发共享动态数据,因此它需要自己的同步设计。
已确认:RTD 原有合理锁保留。
12.2 RTD 可能包含的并发活动
例如:
producer append data
producer update state
discard old data
observer notification
Prepare read RTD
这些活动之间的同步必须由 RTD 自己保证。
不能为了追求“Render DAG 无锁”而直接删掉 RTD mutex。
12.3 RTD 的具体锁序
待确认:
需要根据回退后的实际代码逐项写清:
RTD mutation mutex
RTD data/state mutex
observer callback 前后锁释放位置
discard 与 reader 的关系
render read 是否持锁
尤其必须说明 callback 是否在锁外执行。
13. State Strategy
普通 Renderable State 和 RTD 不是一个概念。
Renderable State Strategy 负责:
update/cache/render state
publish/swap
revision
Double State / Triple State 的具体同步规则必须按当前 Strategy 实现描述。
13.1 已确认原则
State 的 publish / swap 本身允许使用锁。
不能把:
发布动作使用 mutex
误判为:
Render DAG worker 因读取普通数据而加锁
这两种锁语义完全不同。
13.2 待确认
需要根据回退版本最终写明:
- render state getter 是否直接读 render state。
- publish/swap 是否加锁。
- cache/update state 谁能写。
- observer 是锁内还是锁外调用。
- Triple State 的 published/render/cache 三者什么时候交换。
14. Observer / callback
Callback 是线程同步中最容易出现重入问题的地方。
原则:
不应该在持有可能被 callback 重入的内部数据锁时执行未知用户 callback。
典型安全模式:
lock
修改内部状态
准备 Observation
unlock
调用 observer
而不是:
lock
修改内部状态
调用 observer
unlock
14.1 待确认
需要结合实际代码列出:
State observer
RTD observer
Frame observer
Renderable callback
分别在哪个线程执行,以及调用时持有哪些锁。
15. Scene worker 与 Taskflow worker
待结合回退代码最终确认线程名称和数量,但概念上需要明确区分两层:
15.1 Scene render 生命周期线程
负责一帧的高层流程,例如:
开始一帧
组织/提交 Render Plan
启动 Taskflow
等待 Taskflow
完成 Frame
15.2 Taskflow executor workers
真正执行 DAG node:
Prepare node
Paint node
Composite node
因此某个 mutex 被 Scene caller / Scene worker 持有,不等于 Taskflow nodes 被这把 mutex 串行化。
分析锁竞争时必须说明:
谁拿锁
谁等待锁
锁是否进入 Taskflow node
不能只看到“整帧期间存在一把锁”就判断它影响 DAG 并行度。
16. 当前锁分类
锁应按用途分类,而不是简单分为“好锁/坏锁”。
| 类型 | 典型用途 | 是否允许 |
|---|---|---|
| Frame Strategy 生命周期锁 | render 与 publish/swap 互斥 | 允许 |
| Frame/Buffer ownership 锁 | buffer 生命周期、队列、交换 | 允许 |
| RTD 数据锁 | 动态共享实时数据 | 允许 |
| State publish/swap 锁 | 状态发布和交换 | 允许 |
| Scene topology/control 锁 | attach/detach/控制操作 | 允许 |
| Observer 内部管理锁 | observer 注册/状态维护 | 按实际实现 |
| viewport 读取锁 | DAG 内读取 viewport | 不应该有 |
| configuration 读取锁 | DAG 内读取普通配置 | 不应该有 |
| render graph 读取锁 | DAG 内读取当前 graph | 不应该有 |
| Paint 全局锁 | 串行化所有 Paint | 不应该有 |
| Prepare 全局锁 | 串行化所有 Prepare | 不应该有 |
17. “内部渲染无锁”的准确含义
项目里的“清理内部渲染锁”不能理解成:
整个 Renderive 在 Frame render 期间任何地方都不能存在 mutex。
正确含义应该是:
Render DAG 不因为读取本来已经由现有线程契约保证稳定的普通渲染数据而重复获取 mutex。
明确包括:
viewport
configuration
render graph
不应该靠新的 mutex 保护 DAG 内读取。
但不包括:
Frame Strategy 生命周期互斥
Frame publish/swap
RTD 动态共享数据同步
State publish/swap
Scene 控制操作
这些同步有独立语义,不能因为“内部渲染无锁”被删除。
18. 当前禁止擅自引入的改动
在没有重新确认线程模型之前,不进行以下改动:
不把 Render_Lease 擅自改成短 claim
不把 Frame Strategy 生命周期锁改成 CAS
不重新设计三缓冲 ownership
不删除 RTD 正常数据锁
不为 viewport 新增 mutex/atomic
不为 configuration 新增 mutex 兜底
不为 render graph 新增 worker-side mutex
不引入新的“统一快照层”改变原有函数语义
后续任何锁优化必须先证明:
这把锁保护的具体对象是什么
谁会并发访问
现有更高层同步是否已经保证安全
删除后是否改变旧函数语义
19. 需要项目设计者确认的线程契约
下面这些点需要在最终版本中由代码或设计者明确,确认后再更新本文,不应由实现者自行猜测:
viewport修改允许在哪些线程发生。- Frame render 期间是否允许修改 viewport。
- Renderable configuration 的所有修改入口及其线程约束。
rebuild_render_graph()的调用线程和真正 rebuild 时机。- 当前 graph 的生命周期如何保证。
- Scene 高层 render 生命周期实际在哪个线程执行。
- 三种 Frame Strategy 的
Render_Lease分别保护哪些状态。 - 三种 Strategy 的 publish/swap 与 render 的锁顺序。
- RTD 每把锁分别保护什么。
- RTD observer 是否一定在内部数据锁释放以后调用。
- State Strategy 的 render getter 是否无锁。
- Double/Triple State publish/swap 的精确锁范围。
- Scene control lock 与 Frame Strategy lock 是否允许嵌套。
- Frame observer/callback 在哪个线程调用。
- 是否存在任何公开 API 允许在 Render DAG 执行期间修改当前 Renderable 的 graph/config。
20. 文档最终需要达到的目标
最终 threading.md 必须让实现者只看本文就能回答:
某个字段谁写?
某个字段谁读?
允许哪些线程同时访问?
需要哪把锁?
为什么需要?
锁覆盖多大范围?
callback 时是否还持锁?
Frame Strategy 为什么可以整帧持锁?
RTD 为什么需要锁?
viewport/config/graph 为什么不需要额外读锁?
两个 Renderable 为什么可以并行?
什么时候必须通过 DAG edge 而不是 mutex 建立顺序?
只有这些问题全部能从文档直接得到确定答案以后,才根据文档做下一轮锁清理。