五、C# Job System中NativeContainer

本文探讨Unity的Job System如何利用NativeContainer优化多线程任务处理。NativeContainer如NativeArray、NativeList等允许作业访问共享数据,而不必复制。安全系统确保数据访问的安全,避免竞争条件,同时提供不同类型的内存分配策略,以适应各种作业需求。

NativeContainer

 the safety system’s复制数据的过程的缺点是它还隔离了每个副本中作业的结果。 要克服此限制,您需要将结果存储在一种名为NativeContainer的共享内存中。

What is a NativeContainer?

NativeContainer是一种托管值类型,为本机内存提供相对安全的C#包装器。 它包含指向非托管分配的指针。 与Unity C#作业系统一起使用时,NativeContainer允许作业访问与主线程共享的数据,而不是使用副本。

有哪些类型的NativeContainer?

Unity附带一个名为NativeArray的NativeContainer。 您还可以使用NativeSlice操作NativeArray,以将NativeArray的子集从特定位置获取到特定长度。

注意:实体组件系统(ECS)包扩展Unity.Collections命名空间以包含其他类型的NativeContainer:

NativeList  - 可调整大小的NativeArray。
NativeHashMap  - 键和值对。
NativeMultiHashMap  - 每个键有多个值。
NativeQueue  - 先进先出(FIFO)队列。

NativeContainer and the safety system(安全系统)

the safety system内置于所有NativeContainer类型中。它跟踪正在读取和写入任何NativeContainer的内容。

注意:对NativeContainer类型的所有安全检查(例如越界检查,释放检查和竞争条件检查)仅在Unity编辑器和播放模式下可用。

该the safety system的一部分是DisposeSentinel和AtomicSafetyHandle。 DisposeSentinel检测到内存泄漏,如果您没有正确释放内存,则会出错。泄漏发生后很久就会发生内存泄漏错误。

使用AtomicSafetyHandle在代码中转移NativeContainer的所有权。例如,如果两个预定作业正在写入同一个NativeArray,则安全系统会抛出一个异常,并显示一条明确的错误消息,说明解决问题的原因和方法。安排违规工作时,安全系统会抛出此异常。

在这种情况下,您可以安排具有依赖关系的作业。第一个作业可以写入NativeContainer,一旦完成执行,下一个作业就可以安全地读取和写入同一个NativeContainer。从主线程访问数据时,读写限制也适用。安全系统允许多个作业并行读取相同的数据。

默认情况下,当作业可以访问NativeContainer时,它具有读写访问权限。此配置可能会降低性能。 C#作业系统不允许您安排具有对NativeContainer具有写访问权限的作业,而该作业与写入该作业的另一个作业同时进行。

如果job不需要写入NativeContainer,请使用[ReadOnly]属性标记NativeContainer,如下所示:

[ReadOnly]
public NativeArray<int> input;

 

在上面的示例中,您可以与其他具有对第一个NativeArray的只读访问权限的作业同时执行作业。

注意:无法防止从job中访问静态数据。 访问静态数据会绕过所有安全系统,并可能导致Unity崩溃。 有关更多信息,请参阅C# Job System提示和故障排除。

NativeContainer Allocator

创建NativeContainer时,必须指定所需的内存分配类型。分配类型取决于作业运行的时间长度。通过这种方式,您可以定制分配以在每种情况下获得最佳性能。

NativeContainer内存分配和释放有三种Allocator类型。在实例化NativeContainer时需要指定适当的一个。

Allocator.Temp分配最快。

它适用于寿命为一帧或更少的分配。您不应该使用Temp将NativeContainer分配传递给作业。您还需要在从方法调用返回之前调用Dispose方法(例如MonoBehaviour.Update,或从本机代码到托管代码的任何其他回调)。


Allocator.TempJob是比Temp更慢的分配,但比Persistent更快。

它适用于四帧生命周期内的分配,并且是线程安全的。如果不在四帧内处理它,控制台将打印一个从本机代码生成的警告。大多数小作业使用此NativeContainer分配类型。


Allocator.Persistent是最慢的分配,但只要你需要它,并且如果有必要,可以持续整个应用程序的生命周期。

它是直接调用malloc的包装器。较长的作业可以使用此NativeContainer分配类型。在性能至关重要的情况下,不应使用Persistent。

For example:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

 注意:上例中的number 1表示NativeArray的大小。 在这种情况下,它只有一个数组元素(因为它只在结果中存储一个数据)。

来自:https://docs.unity3d.com/Manual/JobSystemNativeContainer.html

Unity 的 **C# Job System** 是一种用于优化性能的多线程任务处理系统,它允许开发者将计算任务分割为多个 **Job**(作业),并在多个 CPU 核心上并行执行。由于 Unity 引擎自动管理线程池和同步机制,开发者可以专注于任务逻辑的实现,而无需处理底层的线程管理[^2]。 ### 核心组件与概念 1. **Job**:Job 是一个轻量级的任务,它可以在多个线程上并行执行。Job 需要实现 `IJob` 接口或其派生接口(如 `IJobParallelFor`、`IJobParallelForTransform`)。 2. **NativeContainer**:为了确保 Job 安全地访问数据,Unity 提供了 `NativeArray<T>`、`NativeList<T>` 等原生容器,这些容器支持跨线程访问且不会产生垃圾回收(GC)负担。 3. **JobHandle**:每个提交的 Job 都会返回一个 `JobHandle` 对象,它用于跟踪 Job 的执行状态,并支持依赖链的构建,确保 Job 按照正确的顺序执行。 4. **Scheduling**:Job 的调度由 Unity 自动管理,开发者可以通过 `JobHandle` 来控制调度顺序,实现依赖关系。 ### 基本使用流程 以下是一个使用 `IJobParallelFor` 的简单示例,展示如何在多个元素上并行执行操作: ```csharp using Unity.Burst; using Unity.Jobs; using Unity.Collections; using UnityEngine; public class JobExample : MonoBehaviour { void Start() { int length = 1000; NativeArray<float> values = new NativeArray<float>(length, Allocator.TempJob); // 定义一个 Job var job = new MyJob { Values = values }; // 调度 Job JobHandle handle = job.Schedule(length, 64); // 64 为批处理大小 // 等待 Job 完成 handle.Complete(); // 读取结果 for (int i = 0; i < length; i++) { Debug.Log(values[i]); } values.Dispose(); // 释放原生容器 } [BurstCompile] struct MyJob : IJobParallelFor { public NativeArray<float> Values; public void Execute(int index) { Values[index] = Mathf.Sin(index * 0.1f); } } } ``` ### 最佳实践 1. **合理划分 Job 粒度**:Job 的粒度不宜过小,否则调度开销可能超过实际计算收益。建议将任务划分为多个中等大小的 Job,以充分利用 CPU 资源。 2. **使用 Burst 编译器**:通过 `[BurstCompile]` 属性可以显著提升 Job 的执行效率,Burst 将 C# 编译为高度优化的本地代码[^1]。 3. **避免主线程阻塞**:尽量避免频繁调用 `JobHandle.Complete()`,特别是在 Update 方法中。可以使用依赖链来异步处理多个 Job。 4. **使用 NativeContainer 管理数据**:确保所有在 Job 中访问的数据都封装在 `NativeArray`、`NativeList` 等容器中,以避免内存安全问题。 5. **控制调度频率**:对于频繁执行的小型任务,建议使用 `IJobParallelFor` 并设置合适的批处理大小(batch size),通常 32 到 1024 之间较为合适。 ### 多线程与依赖管理 Unity Job System 支持 Job 之间的依赖关系管理。例如: ```csharp JobHandle firstJobHandle = firstJob.Schedule(...); JobHandle secondJobHandle = secondJob.Schedule(..., firstJobHandle); secondJobHandle.Complete(); ``` 上述代码确保了 `secondJob` 在 `firstJob` 完成之后才开始执行。这种机制非常适用于流水线式的任务处理逻辑。 ### 性能调优建议 - **分析 Job 执行时间**:使用 Unity 的 Profiler 工具监控 Job 的执行时间,识别瓶颈。 - **减少主线程与 Job 之间的数据同步**:避免频繁地从主线程读取 Job 结果,尽可能在 Job 中完成数据处理。 - **合理使用 Allocator.TempJob**:该分配器专为 Job 设计,生命周期短,适合临时数据使用。 ### 常见问题与注意事项 - **GC 内存不能在 Job 中使用**:所有在 Job 中使用的数据结构必须为非托管类型或使用 `NativeContainer`。 - **避免竞态条件**:多个 Job 同时写入同一数据会导致未定义行为,应使用适当的同步机制或设计无共享数据的 Job。 - **注意 Job 的执行顺序**:除非显式设置依赖关系,Job 的执行顺序是不确定的。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值