Unity C# Job System

作者:追风剑情 发布于:2020-9-14 11:34 分类:Unity3d

官方文档 https://docs.unity3d.com/cn/Manual/JobSystemOverview.html

Unity C# Job System (Unity C#作业系统)
1、通过Job System用户可以和Unity引擎共享工作线程(worker threads),而不必额外创建自己的线程来处理作业,从而避免创建线程过多产生过多的上下文切换带来的CPU消耗。
2、Job System将作业放入作业队列以待执行,Job System中的工作线程从作业队列中获取并执行作业项,Job System会管理依赖关系确保以适当的顺序执行作业。
3、Job System中的作业持有数据副本,必免了竞争条件。
4、Job System是在本地代码层执行,用户的代码是在托管代码层执行。意味着作业只能访问blittable 数据类型。
5、Job System使用memcpy复制blittable类型,并在Unity的托管部分和本机部分之间传输数据。在调度作业时,系统使用memcpy将数据放入本机内存,并在执行作业时让托管端访问副本。
6、Joy System不允许两个作业同时对同一个NativeContainer进行写入。
7、无法防止从作业中访问静态数据。访问静态数据时会绕过所有安全系统,并可能导致Unity崩溃。
8、JobHandle.Schedule方法不会执行作业(只能在主线程中调用Schedule())。
9、JobHandle.Complete方法会在内存缓存中刷新作业并启动执行过程(只能在主线程中调用Complete())。
10、在JobHandle上调用Complete会将该作业的NativeContainer类型的所有权交还给主线程。
11、如果不需要访问结果,请调用静态方法JobHandle.ScheduleBatchedJobs(),此方法会对性能产生负面影响。在实体组件系统(ECS)中会隐式刷新批次,因此没必要调用JobHandle.ScheduleBatchedJobs()。
12、实现IJob的作业采用线性执行,实现IJobParalleFor的作业采用并行执行。
13、请使用 JobHandle 来管理依赖关系,而不是尝试在作业中调度作业。
14、对只读的NativeContainer类型加上[ReadOnly]标记可提高性能。
15、作业具有一个 Run 函数,可以使用此函数代替 Schedule 立即在主线程上执行作业。可将此函数用于调试。
16、不要在作业中分配托管内存。在作业中分配托管内存非常慢,而且作业无法使用 Unity Burst 编译器来提高性能。

NativeContainer
安全系统复制数据的过程的缺点是会将作业的结果隔离到每个副本中。为了克服此限制,需要将结果存储在一种名为 NativeContainer 的共享内存中。
NativeContainer 是一种托管值类型,为本机内存提供了一个相对安全的 C# 封装器。它包含一个指向非托管分配的指针。与 Unity C# 作业系统一起使用时,NativeContainer 允许作业访问与主线程共享的数据,而不是使用副本。

可用的NativeContainer 类型:
NativeArray
NativeList 可调整大小的 NativeArray
NativeHashMap 键/值对
NativeMultiHashMap 每个键有多个值
NativeQueue 先进先出 (FIFO) 队列

[ReadOnly]
public NativeArray<int> input;

NativeContainer Allocator(内存分配类型)
Allocator.Temp 具有最快的分配速度。此类型适用于寿命为一帧或更短的分配。不应该使用 Temp 将 NativeContainer 分配传递给作业。在从方法调用(例如 MonoBehaviour.Update 或从本机代码到托管代码的任何其他回调)返回之前,还需要调用 Dispose 方法。
Allocator.TempJob 的分配速度比 Temp 慢,但比 Persistent 快。此类型适用于寿命为四帧的分配,并具有线程安全性。如果没有在四帧内对其执行 Dispose 方法,控制台会输出一条从本机代码生成的警告。大多数小作业都使用这种 NativeContainer 分配类型。
Allocator.Persistent 是最慢的分配,但可以在您所需的任意时间内持续存在,如果有必要,可以在整个应用程序的生命周期内存在。此分配器是直接调用 malloc 的封装器。持续时间较长的作业可以使用这种 NativeContainer 分配类型。在非常注重性能的情况下不应使用 Persistent。
例如:
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);//1:指要创建的数组长度
注意:在设计作业时,请记住它们是对数据副本进行操作,但 NativeContainer 除外。因此,从主线程中的作业访问数据的唯一方法是写入 NativeContainer。

简单作业定义示例
// 将两个浮点值相加的作业
public struct MyJob : IJob
{
    public float a;
    public float b;
    //result中的数据允许主线程读取
    public NativeArray<float> result;

    public void Execute()
    {
        result[0] = a + b;
    }
}

调度作业
要在主线程中调度作业,必须:
1) 实例化该作业。
2) 填充作业的数据。
3) 调用 Schedule 方法。
4) 调用 Schedule 会将该作业放入作业队列中,以便在适当的时间执行。一旦作业已调度,就不能中断作业。

注意:只能从主线程调用 Schedule。

调度作业的示例
// 创建单个浮点数的本机数组以存储结果。此示例等待作业完成,仅用于演示目的
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

// 设置作业数据
MyJob jobData = new MyJob();
jobData.a = 10;
jobData.b = 10;
jobData.result = result;

// 调度作业
JobHandle handle = jobData.Schedule();

// 等待作业完成
handle.Complete();

// NativeArray 的所有副本都指向同一内存,您可以在"您的"NativeArray 副本中访问结果
float aPlusB = result[0];

// 释放由结果数组分配的内存
result.Dispose();

JobHandle和依赖项
JobHandle firstJobHandle = firstJob.Schedule();
secondJob.Schedule(firstJobHandle);//firstJob执行完后才能执行secondJob

合并多个依赖项
NativeArray<JobHandle> handles = new NativeArray<JobHandle>(numJobs, Allocator.TempJob);
// 使用来自多个调度作业的 `JobHandles` 填充 `handles`...
//...
JobHandle jh = JobHandle.CombineDependencies(handles);

多个作业和依赖项的示例
作业代码:
// 将两个浮点值相加的作业
public struct MyJob : IJob
{
    public float a;
    public float b;
    public NativeArray<float> result;

    public void Execute()
    {
        result[0] = a + b;
    }
}

// 将一个值加一的作业
public struct AddOneJob : IJob
{
    public NativeArray<float> result;
    
    public void Execute()
    {
        result[0] = result[0] + 1;
    }
}

主线程代码:
// 创建单个浮点数的本机数组以存储结果。此示例等待作业完成
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

// 设置作业 #1 的数据
MyJob jobData = new MyJob();
jobData.a = 10;
jobData.b = 10;
jobData.result = result;

// 调度作业 #1
JobHandle firstHandle = jobData.Schedule();

// 设置作业 #2 的数据
AddOneJob incJobData = new AddOneJob();
incJobData.result = result;

// 调度作业 #2
JobHandle secondHandle = incJobData.Schedule(firstHandle);

// 等待作业 #2 完成
secondHandle.Complete();

// NativeArray 的所有副本都指向同一内存,您可以在"您的"NativeArray 副本中访问结果
float aPlusB = result[0];

// 释放由结果数组分配的内存
result.Dispose();


调度 ParallelFor 作业的示例
作业代码
// 将两个浮点值相加的作业
public struct MyParallelJob : IJobParallelFor
{
    [ReadOnly]
    public NativeArray<float> a;
    [ReadOnly]
    public NativeArray<float> b;
    public NativeArray<float> result;

    public void Execute(int i)
    {
        result[i] = a[i] + b[i];
    }
}

主线程代码:
NativeArray<float> a = new NativeArray<float>(2, Allocator.TempJob);

NativeArray<float> b = new NativeArray<float>(2, Allocator.TempJob);

NativeArray<float> result = new NativeArray<float>(2, Allocator.TempJob);

a[0] = 1.1;
b[0] = 2.2;
a[1] = 3.3;
b[1] = 4.4;

MyParallelJob jobData = new MyParallelJob();
jobData.a = a;  
jobData.b = b;
jobData.result = result;

// 调度作业,为结果数组中的每个索引执行一个 Execute 方法,且每个处理批次只处理一项
JobHandle handle = jobData.Schedule(result.Length, 1);

// 等待作业完成
handle.Complete();

// 释放数组分配的内存
a.Dispose();
b.Dispose();
result.Dispose();

ParallelForTransform 作业
ParallelForTransform 作业是另一种 ParallelFor 作业;专为操作变换组件而设计。

注意:ParallelForTransform 作业是 Unity 中对于任何实现 IJobParallelForTransform 接口的作业的统称。

标签: Unity3d

Powered by emlog  蜀ICP备18021003号   sitemap

川公网安备 51019002001593号