# 前言

最近一直在忙着在新公司项目的事,从 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 类型作为数据。
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:一个函数返回前的短生命周期
  • 例如
    • NativeArraynativeArray = new NativeArray(arrays,Allocator.TempJob);

# 使用方式

主要是 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