英文原文:
https://dotsplayground.com/2020/03/customnativecontainerpt4/
介绍
在本系列的上一篇文章 Custom Native Container [Part 3]: Parallel Job Using Min Max 中,我们添加了对并行Job 的支持。但这些Job仅限于写入数组的单个索引。在本文中,我们将通过添加对 ParallelWriter 的支持来消除 NativeIntArray 的这一限制。本文假定具有基本 (C#) 多线程知识。
1) ParallelWriter 结构
首先,我们必须在我们的NativeIntArray结构中添加一个ParallelWriter结构。这本质上是一个新的容器,只允许向数组写入,但允许多个线程这样做。实际的写操作是通过Interlocked类实现的。这个类提供了原子操作。更多信息可以在这里找到
/*
* ... More Code ...
*/
// 允许在并行Job中通过 NativeIntArray.ParallelWriter 进行并行写入。
// 不允许读.
[NativeContainerIsAtomicWriteOnly]
[NativeContainer]
unsafe public struct ParallelWriter
{
// 完整容器的副本指针。
[NativeDisableUnsafePtrRestriction] internal void* m_Buffer;
internal int m_Length;
// 复制安全句柄。不需要复制 dispose 哨兵,因为在此结构中不会分配任何内存。
#if ENABLE_UNITY_COLLECTIONS_CHECKS
internal AtomicSafetyHandle m_Safety;
#endif
// 为方便起见复制长度
public int Length => m_Length;
public int Increment(int index)
{
// 增量仍然需要安全检查写入权限和索引范围。
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety);
if (index < 0 || index > Length)
throw new IndexOutOfRangeException(string.Format("Index {0} is out of range of '{1}' Length.", index, Length));
#endif
// 增量被实现为原子操作,因为它可以由多个线程同时递增。
return Interlocked.Increment(ref *((int*)m_Buffer + index));
}
public int Decrement(int index)
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety);
if (index < 0 || index > Length)
throw new IndexOutOfRangeException(string.Format("Index {0} is out of range of '{1}' Length.", index, Length));
#endif
return Interlocked.Decrement(ref *((int*)m_Buffer + index));
}
public int Add(int index, int value)
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety);
if (index < 0 || index > Length)
throw new IndexOutOfRangeException(string.Format("Index {0} is out of range of '{1}' Length.", index, Length));
#endif
return Interlocked.Add(ref *((int*)m_Buffer + index), value);
}
}
/*
* ... More Code ...
*/
2) AsParallelWriter
我们定义了一个函数来从我们的容器中创建一个 NativeIntArray.ParallelWriter。下面列出的它的实现应该非常简单。
/*
* ... Previous Code ...
*/
public ParallelWriter AsParallelWriter()
{
ParallelWriter writer;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.CheckWriteAndThrow(m_Safety);
writer.m_Safety = m_Safety;
AtomicSafetyHandle.UseSecondaryVersion(ref writer.m_Safety);
#endif
writer.m_Buffer = m_Buffer;
writer.m_Length = m_Length;
return writer;
}
/*
* ... More Code ...
*/
使用方法
这就是我们实现并行写作所需要的一切。为了证明我们的容器实际上已经能够处理多个写入器,让我们实现一些视觉上有趣的东西。下面的Job在容器中挑选一个随机索引,并并行地增加它的值。随机指数是根据正态分布来挑选的,而不是以条形图的形式画在屏幕上。这就形成了一个高尔顿板!
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
public class NativeIntArraySystem : SystemBase
{
[BurstCompile]
struct ParallelWriteNormalDistributionJob : IJobParallelFor
{
public Random random;
public NativeIntArray.ParallelWriter array;
public void Execute(int index)
{
// 计算正态分布。
double u1 = 1.0 - random.NextDouble();
double u2 = 1.0 - random.NextDouble();
double randomStdNormal = math.sqrt(-2.0 * math.log(u1)) * math.sin(2.0 * math.PI * u2);
double randomNormal = (array.Length / 2) + randomStdNormal * (array.Length / 8);
// 使用正态分布选择要增加的元素。
int arrayIndex = math.clamp((int)randomNormal, 0, array.Length - 1);
// 使用我们的原子操作。
array.Increment(arrayIndex);
}
}
protected override void OnUpdate()
{
NativeIntArray myArray = new NativeIntArray(100, Allocator.TempJob);
// 用正态分布值填充 myArray。
JobHandle jobHandle = new ParallelWriteNormalDistributionJob()
{
random = new Random((uint)UnityEngine.Random.Range(0, int.MaxValue)),
array = myArray.AsParallelWriter()
}.Schedule(10000, 64); // 以 64 个批次(随机选择的值)运行我们的Job 10000 次。
jobHandle.Complete();
// 将 myArray 中的每个元素绘制为条形图,其值是条形的高度。
Job.WithName("DrawBarGraph")
.WithReadOnly(myArray)
.WithoutBurst()
.WithCode(() =>
{
for (int i = 0; i < myArray.Length; i++)
{
float barWidth = 1.0f;
float barHeight = (myArray[i] / 40.0f) * 10.0f;
DrawBar(new float2(i * barWidth, 0), new float2(barWidth, barHeight));
}
}).Run();
myArray.Dispose();
}
private void DrawBar(float2 position, float2 size)
{
UnityEngine.Color color = UnityEngine.Color.red;
float3 lowerBound = new float3(position.xy, 0);
UnityEngine.Debug.DrawLine(lowerBound, lowerBound + new float3(size.x, 0, 0), color);
UnityEngine.Debug.DrawLine(lowerBound, lowerBound + new float3(0, size.y, 0), color);
UnityEngine.Debug.DrawLine(lowerBound, lowerBound + new float3(size.xy, 0) , color);
lowerBound += new float3(size.xy, 0);
UnityEngine.Debug.DrawLine(lowerBound, lowerBound + new float3(-size.x, 0, 0), color);
UnityEngine.Debug.DrawLine(lowerBound, lowerBound + new float3(0, -size.y, 0), color);
}
}
总结
这篇文章展示了如何添加对ParallelWriter的支持。正常的并发数据结构设计是适用的,所以我们可以使用Interlocked类来实现我们的操作。但有一点需要注意的是,我们的容器不是一个被托管的对象,因此不能被锁定在一个线程上。这意味着所有的native container都需要被设计成无锁数据结构。
在下一部分中,我们将研究如何使用线程索引来实现一个新的无锁数据结构。