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