文章目录
- [EntityCommandBuffer 与 JobHandle 及相关机制](#EntityCommandBuffer 与 JobHandle 及相关机制)
-
- [1. EntityCommandBuffer(ECB)](#1. EntityCommandBuffer(ECB))
-
- [1.1 核心概念](#1.1 核心概念)
- [1.2 两种 ECB 变体](#1.2 两种 ECB 变体)
- [1.3 回放(Playback)机制](#1.3 回放(Playback)机制)
- [1.4 与 JobHandle 的关系](#1.4 与 JobHandle 的关系)
- [2. JobHandle](#2. JobHandle)
-
- [2.1 是什么](#2.1 是什么)
- [2.2 依赖链与 Job 调度](#2.2 依赖链与 Job 调度)
- [2.3 常见误区](#2.3 常见误区)
- [3. 相关核心概念](#3. 相关核心概念)
-
- [3.1 EntityManager 结构性更改与主线程](#3.1 EntityManager 结构性更改与主线程)
- [3.2 EntityCommandBufferSystem](#3.2 EntityCommandBufferSystem)
- [3.3 同步点(Sync Point)与 ECS Safety System](#3.3 同步点(Sync Point)与 ECS Safety System)
- [3.4 Barrier 概念](#3.4 Barrier 概念)
- [3.5 SystemState.Dependency](#3.5 SystemState.Dependency)
- [3.6 并行写入 ECB](#3.6 并行写入 ECB)
- [3.7 NativeContainer 的生命周期与依赖](#3.7 NativeContainer 的生命周期与依赖)
- [3.8 主线程阻塞与 Profiler 标记](#3.8 主线程阻塞与 Profiler 标记)
- [4. 使用注意事项总结](#4. 使用注意事项总结)
-
- [ECB 相关](#ECB 相关)
- [JobHandle 相关](#JobHandle 相关)
- 联合使用
EntityCommandBuffer 与 JobHandle 及相关机制
1. EntityCommandBuffer(ECB)
1.1 核心概念
EntityCommandBuffer 是一个线程安全的命令队列,用于延迟执行 ECS 中的结构性更改(Structural Changes),即会改变实体原型(Archetype)的操作,包括:
- 创建 / 销毁实体
- 添加 / 移除 / 设置组件
- 添加 / 移除动态缓冲区(DynamicBuffer)
- 实例化 Prefab
ECS 中的 Job 不允许直接调用 EntityManager 执行结构性更改,因为这会立即改变全局数据结构,破坏并行安全性。因此所有此类操作必须通过 ECB 记录下来,统一在指定时机回放(Playback)。
1.2 两种 ECB 变体
| 类型 | 创建方式 | 使用场景 |
|---|---|---|
| 单线程 ECB | new EntityCommandBuffer(Allocator.TempJob) 或从 EntityCommandBufferSystem.CreateCommandBuffer() |
仅在主线程使用,通常用于 System 的 OnUpdate 中手动记录少量命令,或自定义回放。 |
| 并发 ECB | 通过 SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged) (或类似系统)获得,然后调用 .AsParallelWriter() 获取 ParallelWriter 接口。 |
在多线程 Job 中使用,每个线程安全写入同一个 ECB 的并行写入器。 |
重要 :并发 ECB 不能在多个 Job 之间共享同一个实例,必须传递它的 ParallelWriter(通过 AsParallelWriter() 获得)。一个并发 ECB 可以有一个 ParallelWriter 供多个并行 Job 共同使用。
1.3 回放(Playback)机制
ECB 中记录的命令不会自动执行,需要由 EntityCommandBufferSystem 来播放。
BeginSimulationEntityCommandBufferSystem:在下一帧 Simulation 阶段开始时回放(通常用于大多数逻辑)。EndSimulationEntityCommandBufferSystem:在当前帧 Simulation 阶段末尾回放(用于必须在本帧生效的结构性更改)。- 你还可以创建自定义
EntityCommandBufferSystem,精确控制回放点。
回放时机示例:
- 若你在 Job 中使用
BeginSimulation的 ECB,则命令会在下一帧的 Simulation 开始前执行。这意味着本帧后续的 Job 无法看到这些结构性更改。 - 若你使用
EndSimulation的 ECB 并确保依赖完成,则更改可在当帧稍后的 System 中生效。
1.4 与 JobHandle 的关系
ECB 回放必须等待所有写入该 ECB 的 Job 完成。这通过以下方式自动处理:
- 当你在 System 的
OnUpdate中将 Job 的JobHandle赋值给state.Dependency后,EntityCommandBufferSystem会从注入的 Dependency 中获取依赖,并等待所有使用它的 Job 完成后才执行回放。 - 也就是说,你无需手动为 ECB 添加 JobHandle 依赖,只需确保 Job 的句柄正确链入系统依赖链。
2. JobHandle
2.1 是什么
JobHandle 是 Unity C# Job System 的依赖与同步核心,代表一个或多个异步 Job 的完成状态。它:
- 提供
IsCompleted检查。 - 提供
Complete()强制主线程等待完成。 - 作为
Schedule方法的输入/输出,形成依赖链。
2.2 依赖链与 Job 调度
- 每个
Schedule/ScheduleParallel方法接收一个JobHandle依赖参数,并返回一个新JobHandle。 - 只有当输入依赖完成后,新 Job 才会被调度执行,从而保证数据访问顺序。
- 多个输入依赖可通过
JobHandle.CombineDependencies(handle1, handle2, ...)合并。 - 最终的 JobHandle 应赋值给
SystemState.Dependency,使 ECS 能自动衔接各系统的依赖。
2.3 常见误区
- 忘记传递依赖:导致 Job 无序执行,出现数据竞争。
- 过早 Complete :在 Job 链中途调用
Complete()会强制主线程等待,破坏并行性;应只在真正需要结果时才同步。 - 空 JobHandle :第一个被调度的 Job 可以传入
default(JobHandle),表示无前置依赖。
3. 相关核心概念
3.1 EntityManager 结构性更改与主线程
- 在主线程(System 的
OnUpdate非 Job 部分)可以直接调用EntityManager执行结构性更改,无需 ECB。 - 但高频的结构性更改建议仍然使用 ECB 批量执行,因为 ECB 回放阶段会统一处理并减少同步成本。
3.2 EntityCommandBufferSystem
- ECS 提供的内建 Barrier 系统:
BeginInitializationEntityCommandBufferSystem,EndInitializationEntityCommandBufferSystem,BeginSimulationEntityCommandBufferSystem,EndSimulationEntityCommandBufferSystem等。 - 每个
EntityCommandBufferSystem在帧的固定点执行回放,并清理已播放的 ECB。 - 你可以通过继承
EntityCommandBufferSystem创建自定义播放点,适用于更复杂的帧内阶段控制。
3.3 同步点(Sync Point)与 ECS Safety System
- 当在 Job 中尝试读取尚未完成写入的数据时,会触发同步点(Sync Point),强制主线程等待所有依赖 Job 完成,严重影响性能。
- ECS 的安全系统(Safety System)会检测
NativeContainer的竞争访问,确保只读/只写标记([ReadOnly]、NativeDisableContainerSafetyRestriction)正确使用以避免意外同步。 - 违反安全规则时会抛出异常,但在 Release 构建中这些检查被移除以提高性能。
3.4 Barrier 概念
- "Barrier"通常指一种同步机制,将 Job 流分为前后两部分。
EntityCommandBufferSystem就是一种 Barrier,它将"记录命令"和"应用命令"分开。 - 也可以使用
JobHandle.Complete()或依赖链构造自定义 Barrier,但更推荐利用 ECS 内建系统。
3.5 SystemState.Dependency
- 每个 System 都有一个
Dependency属性,它记录了上一步系统完成工作的JobHandle。 - 在
OnUpdate中,你应该:- 读取
state.Dependency作为输入依赖(或与其他 JobHandle 组合)。 - 将最终输出的 JobHandle 写回
state.Dependency。
- 读取
- 这样 ECS 能够自动管理跨系统的 Job 执行顺序,而无需手动传递 JobHandle。
3.6 并行写入 ECB
- 并发 ECB 通过
AsParallelWriter()方法暴露ParallelWriter,可以在IJobEntity、IJobChunk中安全地并行记录命令。 - 每个
ParallelWriter内部使用线程局部存储(TLS)避免竞争,回放时再合并,所以即使多个线程同时写入也不会冲突。 - 一个 ECB 可以只调用一次
AsParallelWriter(),并将同一个ParallelWriter实例传入多个 Job 是安全的。
3.7 NativeContainer 的生命周期与依赖
- 创建
NativeArray/NativeList时使用Allocator.TempJob或Persistent,必须手动Dispose()。 - 使用
NativeContainer作为 Job 数据时,安全系统会自动添加依赖跟踪,确保在容器释放前所有使用它的 Job 都已完成。 - 若手动过早
Dispose容器,会触发ObjectDisposedException。 - 可以通过
NativeList.AsDeferredJobArray()等接口让 Job 等待容器最终长度确定(例如IJobParallelForDefer)。
3.8 主线程阻塞与 Profiler 标记
- 频繁调用
Complete()会在 Profiler 中显示为 "WaitForJobGroup" 的等待,是优化的关键指标。 - 尽量将
Complete()推到帧的末端,或使用 ECB 替换立即的结构性更改需求,以最大化并行度。
4. 使用注意事项总结
ECB 相关
- 并发 ECB 必须传递
ParallelWriter,不能传递 ECB 实例。 - 始终调用
Dispose()释放 ECB(回放系统会自动释放已播放的 ECB,但手动创建的需要显式释放)。 - 记住你的 ECB 属于哪个回放系统,确保命令在正确的时机生效。
- 同一帧内,若需要让后续 Job 看到 ECB 的更改,务必使用
EndSimulationEntityCommandBufferSystem或自定义早期回放系统,并保证依赖关系正确。
JobHandle 相关
- 永远不要遗漏依赖传递,使用
CombineDependencies处理多个输入。 - 在 ECS System 中,将最终 JobHandle 赋值给
state.Dependency。 - 对于不需要结构性更改的场景,尽量用 Job 链代替 ECB + 回放,以减少开销。
联合使用
- 结构性更改(ECB)与数据计算(Job)要明确划分阶段:先并行计算,再通过 Barrier(ECB System)应用更改,最后让后续 System 读取新状态。
- 利用 ECS 的 System 分组(如
[UpdateInGroup(typeof(SimulationSystemGroup))])和屏障系统,可以自然地将帧切分为多个阶段,每个阶段内部并行,阶段之间同步。
更多内容请查看总目录【Unity】Unity学习笔记目录整理