作业系统与 Burst 编译器搭配使用时效果最佳。由于 Burst 不支持托管对象,因此您需要使用非托管类型来访问作业中的数据。为此,您可以使用 blittable 类型,或使用 Unity 内置的 NativeContainer 对象,这些对象对于本机内存而言是线程安全的 C# 封装器。NativeContainer 对象也允许作业访问与主线程共享的数据,而不是使用副本。
命名空间 Unity.Collections 包含以下内置的 NativeContainer 对象:
NativeArray:非托管数组,会将原生内存缓冲区公开给托管代码。NativeSlice:从特定位置获取到特定长度的 NativeArray 子集。
注意:Collections 包内含其他 NativeContainer。如需了解其他类型的完整列表,请参阅 Collections 文档的 Collection 类型部分。
默认情况下,如果某项作业能够访问 NativeContainer,那么该作业便具有读写权限。这种配置可能会降低性能。这是因为,当一项作业对 NativeContainer 实例进行写入时,作业系统不允许您再调度另一项拥有写入权限的作业。
但如果作业不需要对 NativeContainer 实例进行写入,那么您可以使用 [ReadOnly] 属性标记该 NativeContainer,如下所示:
[ReadOnly]
public NativeArray<int> input;
在上方示例中,您可以在多个作业对首个 NativeArray 拥有只读权限的同时执行该作业。
创建 NativeContainer 实例时,必须指定所需的内存分配类型。使用的分配类型取决于您需要将原生容器保留多长时间的可用性。这样一来,您可以调整分配方案,从而确保在各种情况下都能获得最佳性能。
针对 NativeContainer 的内存分配和释放,存在三种分配器类型。对 NativeContainer 实例进行实例化时,必须指定相应的类型:
Allocator.Temp:最快的分配。适用于生命周期为一帧或更短的分配。您不能使用 Temp 将分配传递给存储在作业成员字段中的 NativeContainer 实例。Allocator.TempJob:分配速度比 Temp 慢,但比 Persistent 快。适用于生命周期为四帧的线程安全分配。重要信息:您必须在四帧内对这种分配执行 Dispose,否则控制台会打印一条由原生代码生成的警告。大多数小型作业都使用这种分配类型。Allocator.Persistent:最慢的分配,但可以按需持续存在,如有必要,甚至可以在应用程序的整个生命周期内存在。它是对 malloc 的直接调用的封装器。持续时间较长的作业可以使用这种 NativeContainer 分配类型。当性能至关重要时,不要使用 Persistent。例如:
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
注意:上例中的数字 1 表示 NativeArray 的大小。此例子中只有一个数组元素,因为在 result 中只存储了一段数据。
所有 NativeContainer 实例中都内置了安全系统。安全系统会跟踪任何对 NativeContainer 实例的读取或写入,并使用这些信息对 NativeContainer 的使用强制执行某些规则,使其在多个作业和线程中以确定的方式运行。
例如,如果两个独立的受调度作业对同一 NativeArray 进行写入,则不安全,因为无法预测哪个作业会被首先执行。这意味着您将不知道哪项作业会覆盖另一项作业的数据。在调度第二项作业时,安全系统会抛出一个异常,并显示一条明确的错误消息,说明问题的原因和解决方法。
如果要调度两项对同一 NativeContainer 实例进行写入的作业,可以调度具有依赖项的作业。第一项作业会对 NativeContainer 进行项,执行完毕后,下一项作业就可以安全地对同一 NativeContainer 进行读取和写入。引入依赖关系可确保作业始终以一致的顺序执行,并确保 NativeContainer 中生成的数据具有确定性。
安全系统允许多项作业并行读取相同的数据。
这些读写限制同样适用于从主线程访问数据的情况。例如,如果您尝试在某项对 NativeContainer 进行写入的作业完成前读取 NativeContainer 的内容,则安全系统会抛出错误。同样,如果您在仍有对 NativeContainer 进行读取或写入的待处理作业时尝试对 NativeContainer 进行写入,那么安全系统也会抛出错误。
此外,由于 NativeContainers 不实现 ref return,您无法直接更改 NativeContainer 的内容。例如,nativeArray[0]++; 等同于写入 var temp = nativeArray[0]; temp++;,后者不会更新 nativeArray 中的值。
正确的做法是,必须将索引中的数据复制到本地临时副本中,修改该副本,然后将其存回。例如:
MyStruct temp = myNativeArray[i];
temp.memberVariable = 0;
myNativeArray[i] = temp;