[Unity Native Container] 自定义Native Container [第 4 部分]:使用 ParallelWriter 的Parallel Job

61 篇文章 4 订阅

英文原文:

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都需要被设计成无锁数据结构。

  在下一部分中,我们将研究如何使用线程索引来实现一个新的无锁数据结构。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值