# 前言
最近一直在忙着在新公司项目的事,从 0 到 1 设计,也是挺考验脑袋的,而且各种有用的技术都想着能不能有所帮助,然后用一用,感觉能写的还是不少,但又似乎感觉没有可成文的深入 (时间) 程度。
不过好像也都有段时间没写点成文的文章了,趁周末稍微整理下 JobSystem 的东西记一记吧 —— ECS 直接用到项目可能存在能不能把握住的问题,现在也不大敢用,JobSystem 这东西后面倒是可以整一整。
而且单独用似乎也挺简单的,用到重要的地方也不会对架构本身产生过大影响 (不像 GPU Driven 类的优化,ComputeShader 还要担心可能存在的兼容性问题)。
# 基础
# Job 调度方式
- Run:直接在主线程执行 (立即)
- Schedule:单个 Worker 线程或主线程执行
- ScheduleParallel:多个 Worker 线程同时执行 (有数据竟态问题)
第一种 Run 方式不提,一般来说用到 Job 的话,就不会仅使用 Run 了,主要执行还是靠后两个接口。
# 计划
Schedule 或 ScheduleParallel,可以称为 『计划』:
- 表示分配一个工作线程
- JobHandler handle=job.Schedule()
# 执行
计划之后则称为执行:
- 实际执行
- handle.Compelete()
# Job 依赖
保证后者可以在被依赖 Job 执行完毕后再执行
可以分为 单个依赖、链式依赖、多个依赖
# 单个依赖
- 例如:
JobHandle handle1=job1.Schedule(); | |
JobHandle handle2=job2.Schedule(handle1); |
# 链式依赖
- 依赖可以将 Job 组织成链式依赖结构,使其按照依赖顺序执行
- 注:若两个 Job 同时依赖一个 Job1,当 Job1 执行完毕,不影响后两个 Job 的并行执行
JobHandle handle1=job1.Schedule(); | |
JobHandle handle2=job2.Schedule(handle1); | |
JobHandle handle3=job3.Schedule(handle1); |
# 多个依赖
- 依赖还可以组织成一个 Job 同时依赖多个 Job 的情况
- 例如:
MyJob job1 = new MyJob(); | |
MyJob job2 = new MyJob(); | |
JobHandle handle1 = job1.Schedule(); | |
JobHandle handle2 = job2.Schedule(); | |
JobHandle handleCombine = JobHandle.CombineDependencies(handle1, handle2); | |
MyJob job3 = new MyJob(); | |
job3.Schedule(handleCombine); |
最终甚至可以组合成依赖关系网,不过必须保证依赖关系网是无环的,否则 Job 的安全检查会报错。
# 数据竟态 (安全检查)
Unity 在检测到发生 Job 访问数据冲突时会抛出异常(仅编辑器生效,可开关,为谨慎性而提供,并不智能)
代码有两种方式使其不进行检查:
- [ReadOnly]:为结构体内字段添加该特性后,表示仅读取,可以同时在多个 Job 中共享
- [NativeDisableContainerSafetyRestriction]:为字段添加该特性,关闭对指定数据安全检查,使其允许多个 Job 同时对一个数据进行读写 (可能会导致有常规的多线程数据不一致问题)
其次可以在编辑器关闭:
# 数据类型
JobSystem 用的也是结构体,配合 NativeArray
Blitable:在 C++ 下与 C# 下内存表述一致的类型
- Jobs 只能使用 值类型 或 非托管堆数据
- C# Bool 值可能被转换为 1、2 或 4 字节的值,不一致 (所以注意 Bool 也不是哦 —— 虽然作为 C# 值类型也可以允许在 NativeArray 使用)
参考:可直接复制到本机结构中的类型和非直接复制到本机结构中的类型 - .NET Framework
- Blitable 数据类型:
- System.Byte
- System.SByte
- System.Int16
- System.UInt16
- System.Int32
- System.UInt32
- System.Int64
- System.UInt64
- System.IntPtr
- System.UIntPtr
- System.Single
- System.Double
- 非 Blitable 数据类型:
- System.Array
- System.Boolean
- System.Char
- System.Class
- System.Object
- System.String
- System.Valuetype
# 数据分配
- Allocation Types
- Allocator.Persistent:长生命周期内存
- Allocator.TempJob:只在 Job 中存在的短生命周期,4 帧以上会收到警告
- Allocator.Temp:一个函数返回前的短生命周期
- 例如
- NativeArray
nativeArray = new NativeArray (arrays,Allocator.TempJob);
- NativeArray
# 使用方式
主要是 4 个接口,其它还有一些
# IJob
最基础方式就是实现 IJob 接口的结构体:
public struct MyJob : IJob | |
{ | |
public NativeArray<int> _native; | |
public NativeArray<int> _native2; | |
public void Execute() | |
{ | |
} | |
} |
使用方式也比较简单,先分配 NativeArray:
int[] dataArray=new int[3]; | |
NativeArray<int> data=new NativeArray<int>(dataArray,Allocator.TempJob); |
然后创建 Job 并赋值数据 (或再次进行计划、设置依赖):
MyJob target1=new MyJob(){source=dataArray}; | |
MyJob target2=new MyJob(){source=dataArray}; | |
JobHandler handle1=target1.Schedule(); | |
JobHandler handle2=target2.Schedule(handle1); |
最后执行:
handle1.Compelte(); | |
handle2.Compelte(); | |
// 最后数据需要拷贝回来 (如果不是直接使用 NativeArray 的话) | |
data.CopyTo(dataArray); |
# IJobFor
上面实现 IJob 接口的,实际上只是分配给另外一个线程 (或主线程空闲时由主线程) 执行,如果想同时多个线程并行执行,则是实现 IJobFor 接口。
在一个线程上执行指定次数
- jobTarget.Run (arrayLength) // 立刻在主线程执行 arrayLength 次
- jobTarget.Schedule (arrayLength,default) // 在单个 worker 线程执行 arrayLength 次
- jobTarget.ScheduleParallel (arrayLength,innerloopBatchCount,default) // 在多个 worker 线程执行 arrayLength 次,innerloopBatchCount 表示每个线程最多可以执行多少次
- innerloopBatchCount 批次:任务比较轻量,使用 32 或 64,任务比较重使用 1
# IJobParallelFor
类似 IJobFor,不过这个接口只提供了 Schedule 方法用于在多个 worker 线程并行执行。
也就是说,它跟 IJob 的区别,大概就在于是否允许仅单个线程执行了吧?
# IJobParallelForTransform
可访问 Transform,使用 TransformAccessArray 添加对 Transform 的引用
改了下官方 IJobParallelFor 的示例:
class TransformJobTest : MonoBehaviour | |
{ | |
struct VelocityJob : IJobParallelForTransform | |
{ | |
[ReadOnly] | |
public NativeArray<Vector3> velocity; | |
public float deltaTime; | |
public void Execute(int index, TransformAccess transform) | |
{ | |
transform.position += velocity[index] * deltaTime; | |
} | |
} | |
private const int _count = 100000; | |
private NativeArray<Vector3> _velocity = new NativeArray<Vector3>(_count, Allocator.Persistent); | |
private TransformAccessArray _transformAccessArray; | |
private void Start() | |
{ | |
for (var i = 0; i < _velocity.Length; i++) | |
_velocity[i] = Random.insideUnitCircle; | |
GameObject o = GameObject.CreatePrimitive(PrimitiveType.Cube); | |
_transformAccessArray = new TransformAccessArray(_count); | |
for (int i = 0; i < _transformAccessArray.capacity; i++) | |
{ | |
_transformAccessArray.Add(GameObject.Instantiate(o).transform); | |
} | |
} | |
public void Update() | |
{ | |
var job = new VelocityJob() | |
{ | |
deltaTime = Time.deltaTime, | |
velocity = _velocity | |
}; | |
JobHandle jobHandle = job.Schedule(_transformAccessArray); | |
jobHandle.Complete(); | |
} | |
private void OnDestroy() | |
{ | |
_velocity.Dispose(); | |
_transformAccessArray.Dispose(); | |
} | |
} |
# BurstCompiler
C#/.NET type support | Burst | 1.8.10
根据官方说法,这个主要就是用来配合 JobSystem 的
- 注意数学库使用:Unity.Mathematics 中提供的
支持的内置类型:
- bool
- byte/sbyte
- double
- float
- int/uint
- long/ulong
- short/ushort
不支持:char、decimal、string
向量类型 (Unity.Mathematics SIMD):
- bool2/bool3/bool4
- uint2/uint3/uint4
- int2/int3/int4
- float2/float3/float4
# 其它
Job 中使用的数学库:Unity.Mathmatics
- 例如随机等常用都有提供
在 Job 中获取执行的线程 Index:
[NativeSetThreadIndex] | |
private int _threadIndex; |
获取 Job 线程最大数量:JobsUtility.MaxJobThreadCount
# 参考文档
- 可直接复制到本机结构中的类型和非直接复制到本机结构中的类型 - .NET Framework
- C#/.NET type support | Burst | 1.8.10
- https://docs.unity.cn/cn/2020.3/ScriptReference/Unity.Jobs.IJobFor.html
- https://www.bilibili.com/video/BV1EV4y1g7ix