美文网首页
C# Job System

C# Job System

作者: 不正经的搬砖工 | 来源:发表于2021-10-21 16:23 被阅读0次

    1、C# Job System特点

    (1)提供C# API,用户可以借助Unity C# Job System更简便的编写多线程代码。

    (2)利用多线程处理数据提高性能。并且Unity将Burst编译器与C# Job System配合使用提高代码生成质量,还可以大大降低移动设备的电耗。

    (3)与Unity的原生作业系统相集成。用户编写的代码与Unity共享工作线程

    2、Job Sytem工作流

    图2-1 Job System工作流程

    (1)每一个Job就是完成一项特定任务的一个工作单位,Job包含要处理的数据以及对数据的操作行为。

    (2)作业系统将创建好的Job放到主线程的一个作业队列(Jobs queue)中。

    (3)作业系统会跨多个核心管理一组工作线程,作业系统会从Jobs queue中提取job并根据job的数据大小和参数将job分发到不同的工作线程(worker thread)。

    其中工作线程个数不超过CPU核心数,避免创建过多争用CPU资源。

    3、C# Job

    (1)Unity中的Job就是实现IJob接口的一个结构体(struct)。

    (2)安全系统和Job中数据类型

    Job中的成员变量数据类型只能为blittable类型或者NativeContainer。

    Unity  C#作业系统为了避免多线程竞争条件导致的错误,实现了一个安全系统,以复制数据的方式避免竞争条件,这就意味着作业只能访问blittable数据类型。但为了解决安全系统操作不同数据副本的限制,引入了NativeContainer类型。

    NativeContainer是一种托管值类型,为本机内存提供了一个相对安全的C#封装器,包含一个指向非托管分配的指针。

    安全系统内置于所有NativeContainer类型中并会跟踪在NativeContainer中读写的内容。

    安全系统包含DisposeSentinel和AtomicSafetyHandle。

    DisposeSentinel 可检测内存泄漏,如果未正确释放内存,则会报错。

    使用AtomicSafetyHandle可以在代码中转移NativeContainer的所有权。保证不同作业可以顺序安全的写入相同的NativeContainer。安全系统运行多个作业并行读取相同数据。

    4、Unity中作业系统应用

    (1)单个作业

    单个作业应用步骤如下:

    创建作业 -> 实例化作业 -> 填充作业数据 -> 调用Schedule方法

    只能从主线程调用schedule,调用Schedule会将该作业放入作业队列以便在适当的时间执行。一旦作业已调度,就不能中断。

    示例如下:

    // 将两个浮点值相加的作业

    public struct MyJob : IJob

    {

        public float a;

        public float b;

        public NativeArray<float> result;

        public void Execute()

        {

            result[0] = a + b;

        }

    }

    // 创建单个浮点数的本机数组以存储结果。此示例等待作业完成,仅用于演示目的

    NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

    // 设置作业数据

    MyJob jobData = new MyJob();

    jobData.a = 10;

    jobData.b = 10;

    jobData.result = result;

    // 调度作业

    JobHandle handle = jobData.Schedule();

    // 等待作业完成

    handle.Complete();

    // NativeArray 的所有副本都指向同一内存,您可以在"您的"NativeArray 副本中访问结果

    float aPlusB = result[0];

    // 释放由结果数组分配的内存

    result.Dispose();

    (2)依赖作业

    调用Schedule方法会返回一个作业JobHandle,如果作业Job2依赖作业Job1,则可以将JobHandle1作为参数传入Job2作业的Schedule方法。示例如下:

    JobHandle firstJobHandle = firstJob.Schedule();

    secondJob.Schedule(firstJobHandle);

    如果一个作业有多个依赖项,则可以使用JobHandle.CombineDependencies方法合并所有依赖项,CombineDependencies可以将所有依赖项传递给Schedule方法。示例如下:

    NativeArray<JobHandle> handles = new NativeArray<JobHandle>(numJobs, Allocator.TempJob);

    // 使用来自多个调度作业的 `JobHandles` 填充 `handles`...

    handles[0] = firstJobHandle;

    handles[1] = secondJobHandle;

    ...

    handles[numJobs - 1] =numJobsHandle;

    JobHandle jh = JobHandle.CombineDependencies(handles);

    (3)ParallelFor 作业

    上述的Job只能一个作业执行一项任务,如果希望一个作业并行执行大量相同的操作可以继承IJobParallelFor接口创建ParallelFor作业。Parallel作业使用一个NativeArray作为数据源,数据源中的每一项都会调用一次Execute方法。如下图所示,C#作业系统会将ParallelFor作业分成多个批次,然后将不同的批次任务分配在不同的核心上。针对每个CPU核心,C#作业系统会在Unity本机作业系统中调度做多一个作业,并向该本机作业传递一些需要完成的批次。

    图5-1  ParallelFor作业在多个核心上分批次调度流程图

    示例如下:

    // 将两个浮点值相加的作业

    public struct MyParallelJob : IJobParallelFor

    {

        [ReadOnly]

        public NativeArray<float> a;

        [ReadOnly]

        public NativeArray<float> b;

        public NativeArray<float> result;

        public void Execute(int i)

        {

            result[i] = a[i] + b[i];

        }

    }

    NativeArray<float> a = new NativeArray<float>(2, Allocator.TempJob);

    NativeArray<float> b = new NativeArray<float>(2, Allocator.TempJob);

    NativeArray<float> result = new NativeArray<float>(2, Allocator.TempJob);

    a[0] = 1.1;

    b[0] = 2.2;

    a[1] = 3.3;

    b[1] = 4.4;

    MyParallelJob jobData = new MyParallelJob();

    jobData.a = a; 

    jobData.b = b;

    jobData.result = result;

    // 调度作业,为结果数组中的每个索引执行一个 Execute 方法,且每个处理批次只处理一项

    JobHandle handle = jobData.Schedule(result.Length, 1);

    // 等待作业完成

    handle.Complete();

    // 释放数组分配的内存

    a.Dispose();

    b.Dispose();

    result.Dispose();

    另外Unity提供了一种特殊的ParallelFor作业:ParallelForTransform作业,专为操作变换组件设计,只需要实现IJobParallelForTransform接口。其中TransformAccessArray 是一个专门用于存储transform的NativeArrray,调用Schedule方法作为参数传给作业,在作业中使用TransformAccess 获取transform。

    示例如下:

    class TransformJobs:MonoBehaviour

        {

            //用于存储transform的NativeArray

            private TransformAccessArray transformAccessArray;

            private NativeArray<Vector3> velocities;

            PositionUpdateJob job;

            JobHandle positionJobHandle;

            struct PositionUpdateJob : IJobParallelForTransform

            {

                public float dt;

                //给每个物体设置一个速度

                [ReadOnly]

                public NativeArray<Vector3> velocity;

                public void Execute(int i, TransformAccess transform)

                {

                    transform.position += velocity[i] * dt;

                }

            }

            private void Start()

            {

                velocities = new NativeArray<Vector3>(10000, Allocator.TempJob);

                //生成一个球体,作为模板

                var sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);

                //保存transform的数组,用于生成transform的Native Array

                var transforms = new Transform[10000];

                for(int i =0;i<100;i++)

                {

                    for(int j = 0;j<100;j++)

                    {

                        var go = GameObject.Instantiate(sphere);

                        go.transform.position = new Vector3(j, 0, i);

                        transforms[i * 100 + j] = go.transform;

                        velocities[i * 100 + j] = new Vector3(0.1f * j, 0, 0.1f * j);

                    }

                }

                transformAccessArray = new TransformAccessArray(transforms);

            }

            private void Update()

            {

                //实例化Job并传入数据

                job = new PositionUpdateJob()

                {

                    dt = Time.deltaTime,

                    velocity = velocities,

                };

                //调度Job

                positionJobHandle = job.Schedule(transformAccessArray);

            }

            //保证当前帧内Job执行完毕

            private void LateUpdate()

            {

                positionJobHandle.Complete();

            }

            //OnDestroy中释放NativeArray内存

            private void OnDestroy()

            {

                velocities.Dispose();

                transformAccessArray.Dispose();

            }

        }

    5、C#作业系统使用注意事项:

    (1)避免从作业访问静态数据

    (2)NativeContainer标记为只读

    默认情况下,作业对NativeContainer类型具有读写访问权限。对于不需要写入操作的NativeContainer使用[Readobly]标记为只读可以提高性能。

    (3)不要尝试更新NativeContainer内容

    由于缺少ref返回值,因此无法直接更改NativeContainer的内容。例如nativeArray[0]++和var temp = nativeArray[0];temp++;都不会更新nativeArray中的值。

    正确的做法是拷贝一个副本,修改完后再赋值给nativearray,示例如下:

    MyStruct temp = myNativeArray[i];

    temp.memberVariable = 0;

    myNativeArray[i] = temp;

    (4)不要在作业中分配托管内存

    在作业中分配托管内存非常慢,而且作业无法使用 Unity Burst 编译器来提高性能。Burst 是一种新的基于 LLVM 的后端编译器技术,可以简化您的工作。此编译器获取 C# 作业并生成高度优化的机器代码,从而利用目标平台的特定功能。

    6、C# Job System与线程比较

    (1)编码容易

    (2)线程管理与安全

    使用C# Job System无需用户自己考虑以下问题,Unity的安全系统会自动处理:

    a、Resource contention/context switching

    b、Race condition

    (3)数据布局

    cache friendly flat memory layout

    (4)Work with Component

    可以针对Unity中的组件操作,例如继承 IJobParallelForTransform 的作业,专为操作变换组件设计。

    (5)Burst Compiler

    对Job执行函数进行优化

    相关文章

      网友评论

          本文标题:C# Job System

          本文链接:https://www.haomeiwen.com/subject/aomtaltx.html