DOTS中的JOBS理解
前言
随着DOTS1.0的正式发布,作为众多菜鸟程序员中的一个,在下准备开坑DOTS了,希望能一直更新下去,这一篇就纯试试水。
一、JOBS是什么?
DOTS中的基础一环就是JOBS(job system)。当然这个可以脱离DOTS来单独使用,其核心就是多线程编程。JOBS帮助我们封装好了多线程的分配与管理,多线程的调度、依赖等问题。附带一句:如果你个人技术很牛逼,也可以自己实现多线程来加速你的游戏逻辑。
二、使用步骤
1.构建一个JOB结构体
构建一个Struct来继承各种IJOB(Unity提供的),至于用哪一种得看具体的需求,这里示例用的IJobParallelForTransform。这个是Unity提供的,专门用来并行执行Trasform操作的Job类。
代码如下(示例):
struct CubesJob : IJobParallelForTransform
{
[ReadOnly] public float elapsedTime;
public void Execute(int index, TransformAccess transform)
{
var distance = Vector3.Distance(transform.position, Vector3.zero);
transform.localPosition += Vector3.up * math.sin(elapsedTime * 3f + distance * 0.2f);
}
}
注意点:一定要区分JOB的用途,带Parallel字眼的Job都是并行的,即多个线程来同时执行。有一些JOB,可以通过Job调度的方式来并行执行,或者按顺序执行(在单独线程,或者主线程中)。
2.主线程调度
在你自己的主线程代码中构建这个Job,因为是Struct所以也不会有GC问题。这里我在一个MonoBehaviour脚本的Update中创建一个Job并进行调度,同时用Complete方法告诉主线程的这个Update你需要等待JOB执行完,再执行后面的操作。个人估计这里有个类似Task的Wait方法。
void Update()
{
var cubesJob = new CubesJob
{
elapsedTime = Time.time,
};
var cubesJobhandle = waveCubesJob.Schedule(transformAccessArray);
cubesJobhandle.Complete();
}
3.有关Job中能用的数据Blittable类型
因为JOB调度底层是C++中的JOBSystem,所以要使用C#与C++兼容的数据才可以在JOB中访问。
Blittable类型的数据,托管内存与非托管内存具有相同的表现形式。
所以C#传给JOB中的数据,如果是一些非Blittable类型要转一下。
还有一些集合类型的容器类型:NativeArray,NativeSlice,TransformAccess,TransformAccessArray(上面的例子用的),NativeList,NativeHashMap等
这些非托管内存,记得调用Dispose手动释放(非托管内存)
注意: C#中的boolean和C++中的不一样,一个是4字节,一个是1字节,所以不能用。
非托管类型容器类使用时还要注意生命周期的声明。
Persistent:长生命周期内存,创建效率最低
TempJob:Job中存在的短生命周期,存在4帧以上没有释放时,会有警告信息。
Temp:函数内生命周期。
4.JOB之间的依赖关系
可以通过Job调度后得到的Jobhandle来管理JOB之间的顺序以及依赖关系。
当然也可以使用JobA.Complete之后,再调用JobB.Schedule来调度JobB。但是这样会导致线程切换回主线程,再调度JobB这样会有一个线程切换的开销。
最佳方式是JobB.Schedule(JobAHandle),这样就可以实现JobB在Job调度完成之后进行调度。(理解为Task的continueWith吧)
5.JOB Safety Checks
比较粗暴的检测JOB的之间的问题:
数据访问冲突(race condition)
数据访问越界
JOBS依赖关系(环形依赖等)
内存释放泄露
总结
JOB作为DOTS中重要的多线程工具,使用起来感觉相对简单易用。但其实多线程,能够用起来还是很多要注意的,稍不注意就有各种奇奇怪怪的问题。
个人之前很喜欢单线程开发,就因为简单,大脑想问题也简单,出错也容易找。多线程就麻烦多了,而且出错也难从断点找根源。希望JOBS这个工具,能解除这种恐惧。