介绍
在本系列文章的前几部分,我们研究了如何创建一个基本的、可用于Job的自定义Native container。这篇文章将改进我们的NativeIntArray容器,增加对并行Job的支持。这是通过使用一种模式来实现的,在这种模式下,Job被分割成多个范围,每个Job只允许在这个范围内操作。这将数组的访问限制在通过Execute(int index)传递的索引。更多关于这些Job在幕后如何调度的信息可以在这里的Unity文档中找到。
1) 启用支持
我们添加了 [NativeContainerSupportsMinMaxWriteRestriction] 标签来支持这种并行Job。我们还必须创建 m_MinIndex 和 m_MaxIndex 变量,并用我们数组的整个范围初始化它们。这些变量是安全检查所必需的。注意,变量的命名和顺序在这里非常重要!
我们也将利用这个机会快速提醒一下我们的容器大致是什么样子的:一个简单的整数数组。
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
// 这样就可以支持并行Job的检测,每个Job线程
// 只允许对最小和最大之间的指数范围进行操作。
[NativeContainerSupportsMinMaxWriteRestriction]
[NativeContainerSupportsDeallocateOnJobCompletion]
[NativeContainer]
[StructLayout(LayoutKind.Sequential)]
public unsafe struct NativeIntArray : IDisposable
{
[NativeDisableUnsafePtrRestriction] internal void* m_Buffer;
internal int m_Length;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
// NativeContainerSupportsMinMaxWriteRestriction 期望对其可以操作的传递范围进行安全检查。
// 当一个并行Job安排它的批处理Job时,该范围被传递给容器。
internal int m_MinIndex;
internal int m_MaxIndex;
internal AtomicSafetyHandle m_Safety;
[NativeSetClassTypeToNullOnSchedule] internal DisposeSentinel m_DisposeSentinel;
#endif
internal Allocator m_AllocatorLabel;
public NativeIntArray(int length, Allocator allocator, NativeArrayOptions options = NativeArrayOptions.ClearMemory){ /* More Code */ }
static void Allocate(int length, Allocator allocator, out NativeIntArray array)
{
long size = UnsafeUtility.SizeOf<int>() * (long)length;
/* More Code */
array = default(NativeIntArray);
array.m_Buffer = UnsafeUtility.Malloc(size, UnsafeUtility.AlignOf<int>(), allocator);
array.m_Length = length;
array.m_AllocatorLabel = allocator;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
// 默认情况下,Job可以在整个范围内运行。
array.m_MinIndex = 0;
array.m_MaxIndex = length - 1;
DisposeSentinel.Create(out array.m_Safety, out array.m_DisposeSentinel, 1, allocator);
#endif
}
/*
* ... Next Code ...
*/
2) 范围检查
我们需要对代码进行的唯一改动是在访问数组中的一个元素时检查我们是否在范围内。在这个容器中访问数组的所有其他函数都使用[]操作符,因此只需在这个操作符上添加我们的范围检查即可。
/*
* ... Previous Code ...
*/
// 如果安全被禁用,则删除对此函数的调用。
[Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]
private void CheckRangeAccess(int index)
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
// 检查我们是否在此并行批处理Job操作的索引范围内。
if (index < m_MinIndex || index > m_MaxIndex)
{
if (index < Length && (m_MinIndex != 0 || m_MaxIndex != Length - 1))
throw new IndexOutOfRangeException(string.Format(
"Index {0} is out of restricted IJobParallelFor range [{1}...{2}] in ReadWriteBuffer.\n" +
"ReadWriteBuffers are restricted to only read & write the element at the job index. " +
"You can use double buffering strategies to avoid race conditions due to " +
"reading & writing in parallel to the same elements from a job.",
index, m_MinIndex, m_MaxIndex));
// 这不是并行Job,但索引仍然超出范围。
throw new IndexOutOfRangeException(string.Format("Index {0} is out of range of '{1}' Length.", index, Length));
}
#endif
}
public int this[int index]
{
get
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckReadAndThrow(m_Safety);
#endif
CheckRangeAccess(index);
return UnsafeUtility.ReadArrayElement<int>(m_Buffer, index);
}
[WriteAccessRequired]
set
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety);
#endif
CheckRangeAccess(index);
UnsafeUtility.WriteArrayElement(m_Buffer, index, value);
}
}
/*
* ... More Code ...
*/
使用方法
而这一切!我们现在在 NativeIntArray 中添加了对Job Parallel的支持。这方面的一个例子如下所示。
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
public class NativeIntArraySystem : SystemBase
{
[BurstCompile]
struct ParallelWriteRangeJob : IJobParallelFor
{
public Random random;
// 请参阅上一部分,了解如何添加对[DeallocateOnJobCompletion].
[DeallocateOnJobCompletion] public NativeIntArray array;
public void Execute(int index)
{
array[index] = random.NextInt();
}
}
protected override void OnUpdate()
{
NativeIntArray myArray = new NativeIntArray(1024, Allocator.TempJob);
// Fill myArray with random values.
JobHandle jobHandle = new ParallelWriteRangeJob()
{
random = new Random((uint)UnityEngine.Random.Range(0, int.MaxValue)),
array = myArray
}.Schedule(myArray.Length, 64, Dependency); // 批处理大小为 64 的计划。
Dependency = jobHandle;
}
}
总结
这篇文章展示了如何使用一种模式来增加对并行Job的支持,在这种模式下,Job被分割成多个范围。但是这种模式的一个局限性是它不允许多个Job写到同一个索引。在下一部分中,我们将看看如何通过添加对ParallelWriter的支持来实现这一点。