找回密码
 立即注册
查看: 211|回复: 2

关于Unity Dots 1.0版本的学习研究(二)之JobSystem

[复制链接]
发表于 2022-10-27 10:16 | 显示全部楼层 |阅读模式
一、简介

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([ChunkIndexInQuery] 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之间访问的是只读的数据,则不会触发安全检测,此时需要给对应的属性添加[ReadOnly]特性,除此之外,如果不想触发unity的安全检测,可以给目标属性添加[NativeDisableContainerSafetyRestriction] 特性,此时,需要对应属性将变得不安全,但不会触发unity安全检测的异常。
四、结语

关于dots1.0,jobsystem的变化,总的来说,比过往的实现方式简单了不少,很多操作都被简化了,通过IjobChunk和IjobEntity的比较就能明显体会到,说明unity这波更新还是很用心的,离用户更近了一步。
关于dots的学习,笔者也是刚开始学习,难免会出现疏漏,不过未来笔者会持续研究纠正,同时随着研究的深入,也会不断完善本文的知识点!
关于学习技巧,笔者建议做中学,第一次学习主要是先对dots有个整体的宏观的了解,之后则需要在实践中不断完善深入理解其细节。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-10-27 10:22 | 显示全部楼层
需要更多的资料
发表于 2022-10-27 10:28 | 显示全部楼层
笔者后续会逐步完善[机智]
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-7-2 07:09 , Processed in 0.119129 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表