Files
Renderive/Kernel/threading.md
T
2026-08-12 21:31:02 +08:00

11 KiB

Renderive 线程模型

总原则

同一个 Renderive 对象的析构不能和调用方主动发起的普通成员函数并发执行。唯一特意处理的跨生命周期路径是实时数据通知到 Scene:实时数据更新允许与 Scene 析构竞争,Scene_Lifetime 会阻止通知进入已经失效的 Scene。

Frame lease 是线程绑定的独占操作句柄。内置 Painter_LeaseRender_Lease 都显式声明 thread_affine = true。lease 可以在取得它的线程内 move,但不能把仍持有底层互斥锁的 lease 转移到另一线程使用或析构;Frame 内容也不能被多个线程同时操作。

用户实现的 Renderable 任务会由 Taskflow 并行执行。不同 Renderable 如果没有依赖关系可以并行,同一个 Renderable 内部没有依赖边的 Prepare 任务也可以并行。写同一个 Paint Buffer 的节点必须有真实依赖或收敛为单 Paint 节点;不同 Renderable 的独立 Paint Buffer 可以并行。用户任务共享的数据必须由用户自己提供同步;Kernel 只保证自己的 Scene、状态、缓存元数据和 Render DAG 描述不会产生数据竞争。

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

Prepare 与 Paint 分别使用原子 input/output revision,可以和后台渲染并发。渲染开始时捕获 revision,结束后只发布该次捕获值,因此渲染过程中发生的 invalidate 不会丢失;Paint invalidation 不修改 Prepare revision。

configuration() 返回值快照;set_renderable_configuration() 通过 Scene 串行修改内部配置。

Renderable 内部图构建和 rebuild_render_graph()render_graph_mutex_ 串行化。内部不可变图快照只由 Scene 和当前帧持有;build_prepare_graph()build_paint_graph() 回调不得重入同一个 Renderable 的构图或重建操作。Render Plan 历史只保存纯拓扑,不保存捕获 Renderable 的执行闭包。

Capture 请求通过原子 controller 在帧开始时一次性保留配额。每个 Capture Frame 按 Plan 的 execution_index 预分配 slot,不使用 node-id 哈希表;不同 Taskflow worker 只写自己的 slot。失败帧丢弃 Abstract Frame 中的执行数据并归还配额,只有成功完成的不可变 Frame Snapshot 才进入带锁的冷路径 repository 和分析阶段。

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_render_graph() 并发执行,验证不可变内部图快照不会在执行期间失效。
  • 并发 Capture ticket 预留,验证只保留请求数量且失败 ticket 会归还。
  • Capture 失败帧,验证 Abstract Frame 不发布 completed snapshot,下一完整帧继续使用该配额。
  • 实时数据 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;两者不能互相替代。