关于Unity Dots 1.0版本的学习研究(二)之JobSystem
一、简介Dots中的Jobsystem相对独立,而且可以脱离于ECS单独使用,同时也是DOTS提升性能的核心之一,因此笔者选择优先学习,相比于其他模块获得的收益最大!
二、多线程方案
说来惭愧,入行unity这么久,却很少用到多线程来优化性能,正好趁此机会,好好总结一下!目前笔者主要了解到的多线程方案有如下3种:
2.1 C#自带多线程
c#本身就带有比较完善的多线程机制,如thread,threadpool,task等,这也是unity一直都支持的,不过使用过程中,需要注意以下几点:
[*]UnityEngine的API不能在分线程运行;
[*]UnityEngine定义的基本结构(int,float,Struct定义的数据类型)可以在分线程计算,如 Vector3(Struct)可以 , 但Texture2d(class,根父类为Object)不可以;
[*]UnityEngine定义的基本类型的函数可以在分线程运行。
可以看到,c#多线程的使用要格外注意,且unity相关的很多操作需求都不能满足,不过还是能解决一些问题的,如多线程下载,网络消息等,想要了解其更多的信息,可以参考下文链接,非常详细。
2.2 unitask
除了c#的多线程,unity中还有一个比较火的多线程解决方案,就是unitask了,它是网上大佬的开源库,主要是对C#语言自带的task进行了封装,使其不仅可以实现多线程还可以像unity协程一样访问unity api(不过这种模式是运行在主线程),同时还优化了gc等,简单实用,笔者强烈推荐大家使用下,详情可以通过下文视频链接来学习。
2.3 JobSystem
与前两者不同的是,jobsystem是可以融合到unity内部的多线程,它可以在真正的多线程中调用unity内部的api,而这是前两者所做不到的,但它在使用上会麻烦一些。因此推荐和unity引擎关系密切的部分多线程的实现使用job,而其他的则用unitask来实现。
三、JobSystem详解
由于job系统可以独立于dots使用,且已经问世很长时间了,所以网上资料还是蛮多的,笔者查看了一下,1.0中的jobsystem和之前的版本是有很多相似之处的,所以大家可以先了解下之前的,再来了解1.0的,学习起来会快很多,关于老版的jobsystem介绍可以参考下文链接!
3.1 官方学习途径
还是先熟悉官方的demo来了解下大概,demo在系列一中提到过(https://github.com/Unity-Technologies/EntityComponentSystemSamples),在DOTS_Guide\Projects\JobsTutorial文件夹下,官方很贴心的提供了一个查找临近点的demo工程,算法很简单,展示了job的使用,同时分析了性能,大家运行一遍就一目了然了。
图1
除了demo工程外,job相关的文档则可以参考DOTS_Guide\cheatsheet\jobs.md和DOTS_Guide\jobs_tutorial\README.md,说实话,介绍的已经比较清楚了。
3.2 如何实现job类
1.0的文档中主要有如下4种方法:
1)通过实现IJob接口实现。
[*]执行线程为主线程。
[*]执行逻辑方法为Execute(),如果有依赖的Job,可以使用Execute(JobHandler dependJob)。
[*]核心调度方法为schedule()。
[*]疑虑:既然还是在主线程执行,那同放在每个monobehaviour执行,效率不应该一样吗,有差别吗?有,由于job和burst只能作用于非托管内存,所以使用此模式前,需要把要处理的数据转成ECS中的非托管集合类型数据(Native等),同时使用Mathematics里的方法,这些方法以及job是可以通过burst来极大提升执行效率的,其次即使不开启burst,由于目标数据均转化成了排列在一起的非托管数据,缓存命中更高,因此访问效率更高,所以执行效率也是比分散在monobehaviour中执行高很多的。大家也可以看下Job_tutorial中的案例来实际体验一下。
2)通过实现IJobParallelFor接口实现。
[*]执行线程为多线程。由单个线程改为多个线程执行,使用合理,效率提升肯定没的说,现在很多手机都是8核以上的,所以用好了,妥妥的大幅提升效率。
[*]执行逻辑方法为Execute(int index),其中index是执行的任务的索引(范围为0~length-1)。
[*]核心调度方法为schedule(int length,int batch)。其中length指总共执行多少个任务,batch指执行批次,相同批次的索引是连续的,且在一个线程中执行。举个例子,长度为100,batch为10,则会分成10波,每波执行100/10个Execute,而同一波执行的会放在同一个线程中调用,且index是连续,如第一波,index是0~9,第二波index是10~19,依次类推。
上面2个方法可以脱离dots体系直接使用,但接下来的2个实现方法则只能在ECS框架中使用。
介绍前,先同步些关键概念:
[*]chunk。块,unity ecs中会把具备相同组件的实体及其组件放在一起,称之为块。在ECS中,system常常遍历同时处理大量的组件数据,而相同的数据放到一起,一是容易实现内存对齐,二是缓存命中率高,两者结合则可以极大提高运算性能。
[*]archetype。原型,chunk很实用,但也是有大小限制的,不能无限往里面塞,所以即便是实体的组件类型均相同,也不能全部放一起,因此需要分成多个chunk,而这些chunk中的实体及组件组合称之为原型。
明白了上面的概念,下面就好理解多了,那么,接着介绍实现job的方法:
3)通过实现IJobChunk接口实现, 案例代码如图2所示。
[*]按chunk调度任务。
[*]执行逻辑方法Execute(in ArchetypeChunk chunk,int unfilteredChunkIndex,bool useEnableMask,in v128 chunkEnabledMask)。
[*]unfilteredChunkIndex,指chunk的索引。
[*]useEnableMask,如果为true,表示chunk中存在一些实体数据,它的目标组件处于disable状态,需要跳过处理,此时chunkEnabledMask字段将会生效,如果为false, 表示chunk中的所有实体都是满足条件的,此时chunkEnabledMask字段是无效的。
[*]chunkEnabledMask,如果生效,它的第n个2进制位,将代表对应index的实体是否需要忽略,比如1带表第一个实体无效,3的2进制位为11,表示第1,2个实体无效。
[*]调度方法有两类:
[*]非并行按顺序调度job,方法Schedule(EntityQuery query,JobHandle dependsOn),query指查询到的实体数据,dependsOn指需要前置完成的job。
[*]并行调度job,方法ScheduleParallel(EntityQuery query,JobHandle dependsOn)。
图2 job chunk案例
4)通过实现IJobEntity接口实现。与IJobChunk接口功能一致,但是代码更加简洁。案例代码如图3所示。
[*]执行逻辑方法为Execute( int chunkIndex, Entity entity, ...),其中chunkIndex是chunk的索引,entity为目标实体,...可以是需要的组件。相比于jobchunk有如下简化:
[*]jobchunk中需要根据查询来获取对应的组件,但这里只需要把组件加到Execute方法的声明中即可。
[*]对于实体类型的过滤,可以通过在类上添加 WithAll、WithNone等属性来直接筛选。
[*]对于不使能的组件的过滤也不需要像jobchunk中那样通过useEnableMask判断,而是直接过滤掉,总体来说,代码比chunk简化不少。
[*]调度方法有两类:
[*]非并行按顺序调度job,方法Schedule(EntityQuery query,JobHandle dependsOn),query指查询到的实体数据,dependson指需要前置完成的job。
[*]并行调度job,方法ScheduleParallel(EntityQuery query,JobHandle dependsOn)。
图3 job entity案例
3.3 其他注意事项
[*]job之间不能相互调用,也不能访问其他job的数据;
[*]job处理的数据需要为非托管的数据(比如NativeArray等类型,后面系列会介绍到),这部分数据需要创建job时传入,且job不能执行IO操作;
[*]job内的变量的数据结构均为结构体,常用数据类型需要使用dots专用的库,如Mathematics等,这些数据结构是专为dots开发的,执行效率更高;
[*]jobs执行过程中是不能直接调整实体数据的,如果需要调整此部分数据,需要通过EntityCommandBuffer来操作,它将把操作缓存在buffer中,待job执行完毕后,的某个时间点由主线程来操作完成;
[*]关于job调度,其实很简单,创建job部分已提到,new job后,会返回一个jobhandler,直接调用其的schedule或ScheduleParallel即可完成调用,需要注意的是调度后不一定会马上执行,如果过需要等待job执行完毕,需要使用jobhandler的complete,它会等到job完毕后在执行下一步操作;
[*]job只能有主线程调度,job之间不能相互调度;
[*]如果job之间存在依赖关系,可以通过在调度的过程(schedule或scheduleParaller)传入dependsOnJob,设置依赖,也可以在通过等待jobhandler的complete结束在执行另一类job的方式实现;
[*]关于job的安全检测,如果多个job之间访问的是只读的数据,则不会触发安全检测,此时需要给对应的属性添加特性,除此之外,如果不想触发unity的安全检测,可以给目标属性添加 特性,此时,需要对应属性将变得不安全,但不会触发unity安全检测的异常。
四、结语
关于dots1.0,jobsystem的变化,总的来说,比过往的实现方式简单了不少,很多操作都被简化了,通过IjobChunk和IjobEntity的比较就能明显体会到,说明unity这波更新还是很用心的,离用户更近了一步。
关于dots的学习,笔者也是刚开始学习,难免会出现疏漏,不过未来笔者会持续研究纠正,同时随着研究的深入,也会不断完善本文的知识点!
关于学习技巧,笔者建议做中学,第一次学习主要是先对dots有个整体的宏观的了解,之后则需要在实践中不断完善深入理解其细节。 需要更多的资料 笔者后续会逐步完善[机智]
页:
[1]