C# Job System

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执行函数进行优化

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容