104 lines
11 KiB
Markdown
104 lines
11 KiB
Markdown
# Renderive 线程模型
|
|
|
|
## 总原则
|
|
|
|
同一个 Renderive 对象的析构不能和调用方主动发起的普通成员函数并发执行。唯一特意处理的跨生命周期路径是实时数据通知到 Scene:实时数据更新允许与 Scene 析构竞争,`Scene_Lifetime` 会阻止通知进入已经失效的 Scene。
|
|
|
|
Frame lease 是线程绑定的独占操作句柄。内置 `Painter_Lease` 和 `Render_Lease` 都显式声明 `thread_affine = true`。lease 可以在取得它的线程内 move,但不能把仍持有底层互斥锁的 lease 转移到另一线程使用或析构;Frame 内容也不能被多个线程同时操作。
|
|
|
|
用户实现的 Renderable 任务会由 Taskflow 并行执行。不同 Renderable 如果没有依赖关系可以并行,同一个 Renderable 内部没有依赖边的任务也可以并行。用户任务共享的数据必须由用户自己提供同步;Kernel 只保证自己的 Scene、状态、缓存元数据和任务图描述不会产生数据竞争。
|
|
|
|
## Scene
|
|
|
|
`render()` 支持多个调用线程同时提交。Scene 不保存多任务提交队列,而是使用 `task_mutex_` 将提交串行化;后一个 `render()` 会等待当前帧结束后再生成下一帧,因此不会覆盖已有提交。
|
|
|
|
`wait_for_render()` 支持多个等待线程。同一次失败渲染的所有 waiter 都读取同一个 `Render_Completion`,不会由第一个 waiter 消耗异常。若失败帧尚未被任何 waiter 观察就提交了下一帧,失败会保存为 pending exception,并由后续 `wait_for_render()` 报告,不会被新的成功 completion 覆盖。
|
|
|
|
`attach_renderable()`、`detach_renderable()`、`set_display_parent()`、`set_dependency_parent()`、`set_renderable_configuration()` 与后台渲染互斥。它们通过 `lock_render_idle()` 等待 Scene 空闲后再修改 Renderable 集合或拓扑。
|
|
|
|
`topology_snapshot()` 和 `renderable_count()` 可以与 Scene 拓扑修改并发调用。拓扑快照持有 `shared_ptr<const Renderable_Base>`,所以返回以后即使对应对象从 Scene detach,快照内对象生命周期仍然有效。
|
|
|
|
`with_final_color_cache()` 在普通线程上等待后台渲染结束后读取最终缓存。若从 Scene 自己的 render worker 调用,则直接读取当前 worker 可见的缓存。回调在 Scene 的控制锁范围内执行,不应启动另一个线程并等待该线程重新进入同一个 Scene 控制 API。
|
|
|
|
Scene observer 在触发事件的线程同步执行。`render_submitted` observer 中再次调用 `render()` 会登记为 deferred render,在当前 submitted callback 返回并启动当前帧后顺序提交;在该 callback 内调用 `wait_for_render()` 为 no-op,避免同步 observer 自锁。render worker 和 Taskflow executor worker 中调用 `wait_for_render()` 同样为 no-op;这些 render execution 线程调用 `render()` 或拓扑/配置修改接口会直接抛出 `std::logic_error`,不会等待当前帧形成自锁。
|
|
|
|
## Renderable
|
|
|
|
`invalidate_cache()`、`cache_revision()`、`rendered_cache_revision()` 使用原子 revision,可以和后台渲染并发。渲染开始时捕获 revision,结束后只发布该次捕获值,因此渲染过程中发生的 invalidate 不会丢失。
|
|
|
|
`configuration()` 返回值快照;`set_renderable_configuration()` 通过 Scene 串行修改内部配置。
|
|
|
|
`task_graph()` 和 `rebuild_task_graph()` 由 `task_graph_mutex_` 串行化。`task_graph()` 返回不可变 `shared_ptr<const Renderable_Task_Graph>`,旧图在正在执行的帧释放快照前不会失效。`build_task_graph()` 是构图回调,不应在同一个 Renderable 上再次调用 `task_graph()` 或 `rebuild_task_graph()`。
|
|
|
|
Renderable 可以晚于 Scene 析构以完成自身释放,但 `scene()` 只用于调用方已经保证 Scene 生命周期有效的同步访问。不要让 `scene()` 返回的引用与 Scene 析构竞争。Kernel 内部需要跨 Scene 生命周期访问的实时数据路径使用 `Scene_Lifetime::Lease`,不依赖这个裸引用。
|
|
|
|
## Flow 帧策略
|
|
|
|
当前 `Flow_Refresh_Strategy` 不依赖 Boost.Lockfree,也没有其他新增队列依赖。Frame 使用传入的 `std::pmr::memory_resource` 分配,待渲染帧保存在 `std::pmr::deque` 中。
|
|
|
|
多个 painter 线程可以同时调用 `acquire_painter()`。Frame 分配、队列容器操作和 Frame 回收都纳入 `state_mutex_` 的同一同步边界,因此即使调用方传入 `std::pmr::unsynchronized_pool_resource`,Kernel 也不会并发进入该资源。多个 renderer 调用线程也不会产生数据竞争;`render_mutex_` 保证一次只有一个 renderer lease 真正消费队列,所以每个已入队 Frame 最多消费一次。
|
|
|
|
Flow 的并发语义是线程安全队列访问,不是 lock-free 保证。单线程生产时保持 FIFO;多个生产线程时,全局顺序由实际进入 `state_mutex_` 并完成入队的线性化顺序决定。
|
|
|
|
## Manual 帧策略
|
|
|
|
多个 painter 调用由 `painter_mutex_` 串行化。`refresh()` 和 renderer 共用 `render_mutex_`,所以不会在 renderer 正在读取 render frame 时交换 pending/render 指针。Frame 指针和状态切换同时受 `state_mutex_` 保护。
|
|
|
|
Manual 只保证最新 pending frame 被 refresh;并发 producer 产生的中间帧可以按策略语义被后来的 prepared frame 替换,这不是丢帧 bug。
|
|
|
|
## Low Latency 帧策略
|
|
|
|
多个 painter 调用由 `painter_mutex_` 串行化,多个 renderer 调用由 `render_mutex_` 串行化。paint/cache/render 三个 Frame 指针只在 `state_mutex_` 下交换,因此 painter 写入和 renderer 读取不会落到同一个 Frame 上。
|
|
|
|
`set_frequency_hz()`、实时数据通知、pending frame discard、状态读取都和 Frame 指针切换使用同一个 `state_mutex_`。频率的发布状态另外通过 `Frame_Control_Strategy_Base` 的内部 mutex 保护。
|
|
|
|
## 实时数据
|
|
|
|
`Latest_Real_Time_Data` 的 update/snapshot/state 访问由数据 mutex 保护。`History_Real_Time_Data` 的 update/clear/discard/snapshot/state 同样受数据 mutex 保护。`Scene_Lifetime::Lease` 使用活动 lease 计数保护 Scene 生命周期,不再把 lifetime mutex 持有到 Frame Strategy observer callback 结束,因此 observer 中再次通过 Renderable 获取同一 Scene 不会递归锁死。
|
|
|
|
多线程 mutation 还使用独立 `mutation_mutex_` 将一次 mutation 与对应 observer 通知作为一个顺序单元。这样 revision 1 的通知一定先于 revision 2,不会出现较新的实时数据已经通知 Frame Strategy 后,又被较旧 observation 覆盖 `last_real_time_data_update_` 的情况。`mutation_mutex_` 使用 recursive mutex,使 observer 同线程重入实时数据 mutation 时不会因为通知顺序锁自身死锁。
|
|
|
|
实时数据 attachment 持有数据源 `shared_ptr`。bind/unbind 与并发 update 由 observer binding state 同步;Scene 析构和实时数据 update 可以并发,通知在获取不到有效 `Scene_Lifetime::Lease` 时直接跳过。
|
|
|
|
## 状态策略
|
|
|
|
Double/Triple State 的 set/get/publish/acquire/render snapshot 都在策略 mutex 下完成。对外读取返回值快照,不把内部 buffer 引用暴露到锁外。
|
|
|
|
Triple State 的 render acquire 与前台 publish 可以并发;每次 acquire 得到完整的某一个 published revision,不会观察到半更新 State。
|
|
|
|
## Observer
|
|
|
|
默认 `Observer_State` 使用 recursive mutex,将多个线程同时触发的 observer callback 串行化,并允许同一线程的 observer callback 再次进入同一个 Observer_State。
|
|
|
|
Observer callback 是同步调用,不是异步事件队列。回调耗时会直接增加触发线程耗时;用户 observer 自身创建的外部对象生命周期和跨对象锁顺序仍由用户负责。
|
|
|
|
## 多线程测试
|
|
|
|
`tests/renderive/threading/Threading_Contract_Test.cpp` 专门验证线程契约:
|
|
|
|
- 8 个线程同时进入 `Observer_State`,确认 observer callback 最大并发数为 1。
|
|
- Flow 使用 4 producer + 4 consumer,验证所有 Frame 恰好消费一次、无重复、无撕裂、pending 计数归零;另用 8 producer + `std::pmr::unsynchronized_pool_resource` 验证 Frame 分配和回收不会并发进入非线程安全 PMR。
|
|
- Manual 使用 4 producer,同时运行 refresher 和 renderer,验证 Frame 指针交换期间不会读到撕裂 Frame。
|
|
- Low Latency 使用 4 producer,同时运行 renderer、frequency configurator 和实时数据 updater,验证三缓冲 Frame 与状态更新并发安全。
|
|
- Latest 实时数据使用 4 updater + snapshot reader,验证 revision、update count 和 value 快照一致。
|
|
- 实时数据 observer 使用 8 updater,验证 observer 收到的 revision 严格按 mutation revision 递增。
|
|
- History 实时数据同时 update、snapshot、discard,验证容器和统计状态一致。
|
|
- Double State 同时 set/publish、读取 cache 和 render snapshot,验证 State 不出现撕裂。
|
|
- Triple State 同时 set/publish、acquire render state 和读取 cache,验证 published revision 快照一致。
|
|
- Scene 使用 4 个 render submitter,验证并发提交不会覆盖任务,最终 render sequence 与实际执行次数一致。
|
|
- `render_submitted` observer 重入 `render()`,验证重入提交会 deferred 而不是和 Scene worker 的 observer 锁形成死锁。
|
|
- Manual `refresh_succeeded` observer 重入 `acquire_renderer()`,验证用户 callback 运行时不再持有 `render_mutex_`。
|
|
- RTD -> Frame Strategy observer 中再次获取 Renderable 的 Scene,验证 `Scene_Lifetime` lease 不跨用户 callback 持有 lifetime mutex。
|
|
- 第一帧失败且未 wait、随后第二帧成功,验证第一帧异常仍由后续 `wait_for_render()` 报告。
|
|
- detach dependency parent 后自动提升 child,验证被提升的缓存 child 会 invalidate 并重新渲染。
|
|
- Renderable task 中调用 `wait_for_render()`,验证当前 Scene execution thread 不等待自身;同线程调用 `render()` 或配置修改会明确拒绝。
|
|
- Built-in Frame lease 明确声明 `thread_affine = true`,测试固定同线程生命周期契约。
|
|
- Scene render 与 `rebuild_task_graph()` 并发执行,验证不可变任务图快照不会在执行期间失效。
|
|
- 实时数据 update 与 Scene 析构竞争,验证 Scene lifetime binding 不产生 UAF。
|
|
|
|
原有测试另外覆盖 render 期间 cache invalidate、任务图构建期间 rebuild、Scene topology 写入与 snapshot 并发、多个 render failure waiter、RTD unbind 与 update 并发等针对性边界。
|
|
|
|
真实 Taskflow 存在时还会运行独立内部任务并行执行测试;内置 dependency-graph executor 只验证 DAG 关系,不伪造并行能力。
|
|
|
|
Linux/GCC 或 Clang 环境建议同时跑 ThreadSanitizer。普通单元测试负责语义和确定性不变量,TSan 负责检测测试覆盖路径上的实际 data race;两者不能互相替代。
|