Unity Job System介绍
来源:The Unity Job System - YouTube
在Profiler窗口中,除了Main Thread,Unity还根据当前处理器核心数创建了Worker Thread。这些Worker Thread除了执行Unity内部功能外,我们也可以使用一些代码自定义使用它们。
一个基本的Job结构
1 | using Unity.Burst; |
在这个Job中,我们给一个
NativeArray
的每一个数都乘方。一个Job必须声明为struct,并且继承
IJob
接口,还需要声明一个Execute
方法,方法体内部的代码会被worker线程异步执行。
NativeArray<int>
是Unity.Collections
提供的数组,它不属于C#的原生数组(C# managed Array),它是Unmanaged,更好地内存管理,更适用于Job系统。Job系统也是可以使用C# managed objects的,但是需要复杂的处理。Burst编译器无法使用C# managed Object
开启一个Job
1 | //在主线程的某地方 |
注意,只有主线程才可以“Schedule”和“Complete” Jobs。
主线程不可以处理当前正在“Schedule”中的Job里面的数据。例如,我们不可以这样:
1
2
3
4
5 var job = new SquaresJob {Nums = myArray};
JobHandle handle = job.Schedule();
Debug.Log(myArray[0]);//因为Unity Job System有safety check,所以会throws an exception。
handle.Complete();
//Debug.Log(myArray[0]);在Complete之后再处理是可以的。
Job之间的依赖
如果我们想在主线程中开启两个相同的Job,这两个Job引用了相同的NativeArray
1 | //在主线程的某地方 |
我们当然可以将两个Job分开运行:
1 | //在主线程的某地方 |
但是这样会阻塞主线程。
最好的方式是使用Job Dependency:
1 | //在主线程的某地方 |
使用Job Dependency,第一个Job就成了第二个Job的Dependency,当第一个Job完成之时,不会把这个Job从Job Queue中除去,而是等所有它的Job Dependency都Complete之后再执行。而且安全检查也会知道当前Job和它的Dependencies不冲突。
Job Dependency可以为我们的Job设置执行顺序:
A <—— B <—— C <—— D
1 | JobHandle handleA = jobA.Schedule(); |
当然也可以多对一依赖:
1 | JobHandle handleA = jobA.Schedule(); |
可以一对多依赖,使用JobHandle.CombineDependencies
静态方法:
1 | JobHandle handleB = jobB.Schedule(); |
我们可以设计一个非常复杂的Job依赖图:
在上图中,B依赖A,D依赖B和C……以此类推。
如果我们调用handleI.Complete()
,那么jobE和jobF也都会Complete
,我们无需担心
禁止出现循环依赖
不能出现A依赖B,B依赖A的情况,幸运的是,我们不需要担心出现这种情况:一个Job只能依赖在它之前Schedule的Job,一个Job一但Schedule,就不能改变它的Dependence了。
Parallel Job
在一些用例中,我们在一个Job内处理的Array或者List可能是可以并行处理的,这时我们可以使用IJobParalleFor
接口
1 | using Unity.Burst; |
接下来应用这个job
1 | var job = new SquaresJob { Nums = myArray }; |
Batch的容量没有定论,我们需要实际情况实际处理。
寄存器争抢
如果我们将这个整数乘方的Job,创建一千万个整数,然后将Batch Size设为100,整个Job将使用3ms左右
但是如果我们将Batch Size和Array的Length相同,这意味着我们只在一个核心上运行乘方,整个Job一样执行3ms
这是因为我们的执行运算只是乘方,每个核心的计算量虽然不大,但是每个核心会争抢寄存器,这是耗时的,所以像这样的用例,使用单个核心比较好。