什么是JobSystem
并行编程
在游戏开发过程中我们经常会遇到要处理大量数据计算的需求,因此为了充分发挥硬件的多核性能,我们会需要用到并行编程,多线程编程也是并行编程的一种。
线程是在进程内的,是共享进程内存的执行流,线程上下文切换的开销是相当高的,大概有2000的CPU Circle,同时会导致缓存失效,导致万级别的CPU Circle,Job System的设计使用了线程池,一开始先将大量的计算任务分配下去尽量减少线程的执行流被打断,也降低了一些thread的切换开销。获取地址:点击获取
Unreal Unity大部分都是这种模型,分配了一些work thread 然后其他的线程往这个线程塞Task,相比fixed thread模式性能好一些,多出了Task的概念,Unity里称这个为Job。
Unity JobSystem
通常Unity在一个线程上执行代码,该线程默认在程序开始时运行,称为主线程。我们在主线程使用JobSystem的API,去给worker线程下发任务,就是使用多线程。
通常Unity JobSystem会和Burst编译器一起使用,Burst会把IL变成使用LLVM优化的CPU代码,执行效率可以说大幅提升,但是使用Burst时候debug会变得困难,会缺少一些报错的堆栈,此时关闭burst可以看到一些堆栈,更方便debug。虽然并行编程有着种种的技巧,比如,线程之间沟通交流数据有需要加锁、原子操作等等的数据交换等操作。但是Unity为了让我们更容易的编写多线程代码,
通过一些规则的制定,规避了一些复杂行为,同时也限制了一些功能,必要时这些功能也可以通过添加attribute、或者使用指针的方式来打破一些规则。规定包括但不限于:
-
不允许访问静态变量
-
不允许在Job里调度子Job
-
只能向Job里传递值类型,并且是通过拷贝的方式从主线程将数据传输进Job,当Job运行结束数据会拷贝回主线程,我们可以在主线程的job对象访问Job的执行结果。
-
不允许在Native容器里添加托管类型
-
不允许使用指针
-
不允许多个Job同时写入同一个地方
-
不允许在Job里分配额外内存
可以查看 官方文档。
应用场景
基本上所有需要处理数据计算的场景都可以使用,我们可以用它做大量的游戏逻辑的计算,我们也可以用它来做一些编辑器下的工具,可以达到加速的效果。
细节
接口
unity官方提供了一系列的接口,写一个Struct实现接口便可以执行多线程代码,提供的接口包括:
-
IJob:一个线程
-
IJobParallelFor:多线程,使用时传入一个数组,根据数组长度会划分出任务数量,每个任务的索引就是数组元素的索引
-
IJobParallelForTransform:并行访问Transform组件的,这是unity自己实现的比较特殊的读写Transform信息的Job,实测下来用起来貌似worker还是一个在动,但是经过Burst编译后快不少。
-
IJobFor:几乎没用
IJobParallelFor是最常用的,对数据源中的每一项都调用一次 Execute
方法。Execute
方法中有一个整数参数。该索引用于访问和操作作业实现中的数据源的单个元素。
容器
Job使用的数据都需要使用Unity提供的Native容器,我们在主线程将要计算的数据装进NativeContainer里然后再传进Job。主要会使用的容器就是NativeArray,其实就是一个原生的数组类型,其他的容器这里暂时不提这些容器还要指定分配器,分配器包括
-
Allocator.Temp
: 最快的配置。将其用于生命周期为一帧或更少的分配。从主线程传数据给Job时,不能使用Temp分配器。 -
Allocator.TempJob
: 分配比 慢Temp
但比 快Persistent
。在四帧的生命周期内使用它进行线程安全分配。 -
Allocator.Persistent
: 最慢的分配,但只要你需要它就可以持续,如果有必要,可以贯穿应用程序的整个生命周期。它是直接调用malloc. 较长的作业可以使用此 NativeContainer 分配类型。
容器在实现Job的Struct里可以打标记,包括ReadOnly、WriteOnly,一方面可以提升性能,另一方面有时候会有读写冲突的情况,此时应该尽量多标记ReadOnly,避免一些数据冲突。
创建 使用
官方文档已经说的很好。Unity - Manual: Create