该包提供了可用于Job 和 Burst 编译代码的非托管数据结构。
这个包提供的集合分为三类:
-
Unity.Collections 中名称以 Native- 开头的集合类型具有安全检查,以确保它们被正确处理并以线程安全的方式使用。
-
Unity.Collections.LowLevel.Unsafe 中名称以 Unsafe- 开头的集合类型没有这些安全检查。
-
剩余的集合类型没有被分配并且不包含指针,因此实际上它们的处理和线程安全从来都不是问题。这些类型只保存少量数据。
Native-types 执行安全检查以确保传递给它们的方法的索引在边界内,但其他类型在大多数情况下没有。
几个 Native- 类型具有 Unsafe- 等价物,例如NativeList 和 UnsafeList、NativeHashMap 和 UnsafeHashMap 等。
虽然您通常应该更喜欢使用 Native- 集合而不是它们的 Unsafe- 等价物,但 Native- 集合不能包含其他 Native- 集合(由于它们的安全检查的实现)。因此,如果您想要一个列表列表,您可以使用 NativeList<UnsafeList> 或 UnsafeList<UnsafeList>,但不能使用 NativeList<NativeList>。
当安全检查被禁用时,Native-type 和它的 Unsafe-equivalent 之间通常没有显着的性能差异。事实上,大多数 Native- 集合只是作为其 Unsafe- 对应物的包装器实现的。例如,NativeList 由一个 UnsafeList 加上一些安全检查使用的句柄组成。
集合类型
类似数组的类型
核心模块提供了一些关键的类数组类型,包括 Unity.Collections.NativeArray 和 Unity.Collections.NativeSlice。这个包本身提供:
数据结构 | 描述 |
---|---|
NativeList< T > | 一个可调整大小的列表。具有线程和处置安全检查。 |
UnsafeList< T > | 一个可调整大小的列表。 |
UnsafePtrList< T > | 一个可调整大小的指针列表。 |
NativeStream | 一组仅append的、无类型的缓冲区。具有线程和处置安全检查。 |
UnsafeStream | 一组仅append的、无类型的缓冲区。 |
UnsafeAppendBuffer | 仅附加的无类型缓冲区。 |
NativeQueue< T > | 一个可调整大小的队列。具有线程和处置安全检查。 |
UnsafeRingQueue< T > | 固定大小的循环缓冲区。 |
FixedList32Bytes< T > | 一个 32 字节的列表,包括 2 字节的开销,因此有 30 字节可用于存储。最大容量取决于 T。 |
FixedList32Bytes 具有更大尺寸的变体:FixedList64Bytes、FixedList128Bytes、FixedList512Bytes、FixedList4096Bytes。
没有多维数组类型,但您可以简单地将所有数据打包到一维中。例如,对于 int[4][5] 数组,请改用 int[20] 数组(因为 4 * 5 是 20)。
使用 Entities 包时,DynamicBuffer 组件通常是数组或类似列表的集合的最佳选择
另请参阅 NativeArrayExtensions、ListExtensions、NativeSortExtension。
Map 和 Set 类型
数据结构 | 描述 |
---|---|
NativeHashMap<TKey, TValue> | 键值对的无序关联数组。具有线程和处置安全检查。 |
UnsafeHashMap<TKey, TValue> | 键值对的无序关联数组。 |
NativeHashSet< T > | 一组独特的值。具有线程和处置安全检查。 |
UnsafeHashSet< T > | 一组独特的值。 |
NativeMultiHashMap<TKey, TValue> | 键值对的无序关联数组。Key不必是唯一的,即两对可以具有相同的Key。具有线程和处置安全检查。 |
UnsafeMultiHashMap<TKey, TValue> | 键值对的无序关联数组。Key不必是唯一的,即两对可以具有相同的Key。 |
另请参阅 HashSetExtensions、扩展和扩展。
Bit arrays和Bit Fields
数据结构 | 描述 |
---|---|
BitField32 | 一个固定大小的 32 位数组。 |
BitField64 | 一个固定大小的 64 位数组。 |
NativeBitArray | 任意大小的位数组。具有线程和处置安全检查 |
UnsafeBitArray | 任意大小的位数组。 |
字符串类型
数据结构 | 描述 |
---|---|
NativeText | UTF-8 编码的字符串。可变和可调整大小。具有线程和处置安全检查。 |
FixedString32Bytes | 一个 32 字节的 UTF-8 编码字符串,包括 3 字节的开销,因此有 29 字节可用于存储。 |
FixedString32Bytes 具有更大尺寸的变体:FixedString64Bytes、FixedString128Bytes、FixedString512Bytes、FixedString4096Bytes。
另见 FixedStringMethods
其他类型
数据结构 | 描述 |
---|---|
NativeReference< T > | 对单个值的引用。功能上等同于长度为 1 的数组。具有线程和处置安全检查。 |
UnsafeAtomicCounter32 | 一个 32 位原子计数器 |
UnsafeAtomicCounter64 | 一个 64 位原子计数器 |
Job安全检查
Job安全检查的目的是检测Job冲突。如果出现以下情况,两个Job会发生冲突:
- 两个Job访问相同的数据。
- 一项或两项Job都具有对数据的写访问权限。
换句话说,如果两个Job都只有对数据的只读访问权限,就不会有冲突。
例如,您通常不希望一个Job读取数组,而同时另一个Job写入相同的数组,因此安全检查认为这种可能性是冲突的。要解决此类冲突,您必须使一项Job成为另一个Job的依赖项,以确保它们的执行不会重叠。无论您想先运行两个Job中的哪一个,都应该是另一个的依赖项。
当启用安全检查时,每个 Native-collection 都有一个 AtomicSafetyHandle 用于执行线程安全检查。调度Job会锁定Job中所有 Native-collections 的 AtomicSafetyHandle。完成Job会释放Job中所有 Native-collections 的 AtomicSafetyHandle。
当 Native- 集合的 AtomicSafetyHandle 被锁定时:
- 使用集合的Job只有在依赖于所有也使用它的已调度Job时才能被调度。
- 从主线程访问集合会抛出异常。
Job中的只读权限
作为一种特殊情况,如果两个Job都严格读取相同的数据,则它们之间没有冲突,例如如果一个Job从一个数组中读取,而另一个Job也从同一个数组中读取,则不会发生冲突。
ReadOnlyAttribute 将作业结构中的 Native- 集合标记为只读:
public struct MyJob : IJob
{
// This array can only be read in the job.
[ReadOnly] public NativeArray<int> nums;
public void Execute()
{
// If safety checks are enabled, an exception is thrown here
// because the array is read only.
nums[0] = 100;
}
}
将集合标记为只读有两个好处:
- 如果所有使用集合的计划Job都只是只读访问,则主线程仍然可以读取集合
- 如果您安排多个Job对同一集合具有只读访问权限,即使它们之间没有任何依赖关系,安全检查也不会反对。因此,这些Job可以同时运行。
Enumerators
大多数集合都有一个 GetEnumerator 方法,该方法返回 IEnumerator 的实现。枚举器的 MoveNext 方法将其 Current 属性推进到下一个元素。
NativeList<int> nums = new NativeList<int>(10, Allocator.Temp);
// 计算列表中所有元素的总和。
NativeArray<int>.Enumerator enumerator = nums.GetEnumerator();
// 第一个 MoveNext 调用将枚举数前进到第一个元素。
// 当枚举数前进到最后一个元素时,MoveNext 返回 false。
while (enumerator.MoveNext())
{
sum += enumerator.Current;
}
// 数组被处理后,枚举器不再有效使用.
nums.Dispose();
并行的 readers and writers
一些集合类型具有用于从并行Job读取和写入的嵌套类型。例如,要从并行Job安全地写入 NativeList,您需要一个 NativeList.ParallelWriter:
NativeList<int> nums = new NativeList<int>(1000, Allocator.TempJob);
// 并行写入器共享原始列表的 AtomicSafetyHandle
var job = new MyParallelJob {NumsWriter = nums.AsParallelWriter()};
public struct MyParallelJob : IJobParallelFor
{
public NativeList<int>.ParallelWriter NumsWriter;
public void Execute(int i)
{
// A NativeList<T>.ParallelWriter can append values
// but not grow the capacity of the list.
NumsWriter.AddNoResize(i);
}
}
请注意,这些并行读取器和写入器通常不支持集合的全部功能。例如,NativeList 不能在并行Job中增加其容量(因为没有办法安全地允许这种情况而不会产生明显更多的同步开销)。
确定性的读和写
尽管 ParallelWriter 确保并发写入的安全性,但并发写入的顺序本质上是不确定的,因为它取决于线程调度的偶然性(由操作系统和程序控制之外的其他因素控制)。
同样,ParallelReader 虽然保证了并发读取的安全,但是并发读取的顺序本质上是不确定的,所以无法知道哪个线程会读取哪些值。
一种解决方案是使用 NativeStream 或 UnsafeStream,它将读取和写入拆分为每个线程的单独缓冲区,从而避免不确定性。
或者,如果您确定性地将读取划分为单独的范围并在其自己的线程中处理每个范围,则可以有效地获得并行读取的确定性顺序。
如果在写入数据后确定性地对数据进行排序,则还可以有效地获得确定性的写入顺序:
internal partial class MySystemCollections : SystemBase
{
protected override void OnUpdate()
{
// 这个人工示例将所有 Foo 组件值复制到并行列表
// 然后根据 entityInQueryIndex 对列表进行排序
// 使列表中的顺序具有确定性。
// 为简单起见,我们假设我们知道没有
// 超过 100 个带有 Foo 组件的实体。
NativeList<SortableFoo> list = new NativeList<SortableFoo>(100, Allocator.TempJob);
NativeList<SortableFoo>.ParallelWriter writer = list.AsParallelWriter();
Entities.ForEach((int entityInQueryIndex, in Foo foo) =>
{
writer.AddNoResize(
new SortableFoo {SortKey = entityInQueryIndex, Foo = foo}
);
}).ScheduleParallel();
Dependency.Complete(); // Completes the job.
// 因为排序标准不依赖于调度事件,
// 排序后的结果顺序是确定的。
list.Sort(new SortableFooComparer());
// ... Use the sorted list.
}
}
internal struct SortableFoo
{
public int SortKey;
public Foo Foo;
}
internal struct SortableFooComparer : IComparer<SortableFoo>
{
public int Compare(SortableFoo x, SortableFoo y)
{
if (x.SortKey == y.SortKey)
return 0;
return (x.SortKey == y.SortKey) ? -1 : 1;
}
}