Unity C# Job System介绍(四) 并行化Job和故障排除(完结)

并行化job

ParallelFor jobs​docs.unity3d.com

当调度Jobs时,只能有一个job来进行一项任务。在游戏中,非常常见的情况是在一个庞大数量的对象上执行一个相同的操作。这里有一个独立的job类型叫做IJobParallelFor来处理此类问题。ParallelFor jobs当调度Jobs时,只能有一个job来进行一项任务。在游戏中,非常常见的情况是在一个庞大数量的对象上执行一个相同的操作。这里有一个独立的job类型叫做IJobParallelFor来处理此类问题。

注意:“并行化”job是Unity中所有实现了IJobParallelFor接口的结构的总称。

一个并行化job使用一个NativeArray存放数据来作为它的数据源。并行化job横跨多个核心执行。每个核心上有一个job,每个job处理一部分工作量。IJobParallelFor的行为很类似于IJob,但是不同于只执行一个Execute方法,它会在数据源的每一项上执行Execute方法。Execute方法中有一个整数型的参数。这个索引是为了在job的具体操作实现中访问和操作数据源上的单个元素。

一个定义并行化Job的例子:

struct IncrementByDeltaTimeJob: IJobParallelFor
{
    public NativeArray<float> values;
    public float deltaTime;

    public void Execute (int index)
    {
        float temp = values[index];
        temp += deltaTime;
        values[index] = temp;
    }
}

调度并行化job

当调度并行化job时,你必须指定你分割NativeArray数据源的长度。在结构中同时存在多个NativeArrayUnity时,C# Job System不知道你要使用哪一个NativeArray作为数据源。这个长度同时会告知C# Job System有多少个Execute方法会被执行。

在这个场景中,并行化job的调度会更复杂。当调度并行化任务时,C# Job System会将工作分成多个批次,分发给不同的核心来处理。每一个批次都包含一部分的Execute方法。随后C# Job System会在每个CPU核心的Unity原生Job System上调度最多一个job,并传递给这个job一些批次的工作来完成。

一个并行化job划分批次到多个CPU核心

当一个原生job提前完成了分配给它的工作批次后,它会从其他原生job那里获取其剩余的工作批次。它每次只获取那个原生job剩余批次的一半,为了确保缓存局部性(cache locality)。

为了优化这个过程,你需要指定一个每批次数量(batch count)。这个每批次数量控制了你会生成多少job和线程中进行任务分发的粒度。使用一个较低的每批次数量,比如1,会使你在线程之间的工作分配更平均。它会带来一些额外的开销,所以有时增加每批次数量会是更好的选择。从每批次数量为1开始,然后慢慢增加这个数量直到性能不再提升是一个合理的策略。

调度并行化job的例子:

job代码

// Job adding two floating point values together
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;

// Schedule the job with one Execute per index in the results array and only 1 item per processing batch
JobHandle handle = jobData.Schedule(result.Length, 1);

// Wait for the job to complete
handle.Complete();

// Free the memory allocated by the arrays
a.Dispose();
b.Dispose();
result.Dispose();

ParallelForTransform jobs

https://docs.unity3d.com/Manual/JobSystemParallelForTransformJobs.html

一个ParallelForTransform job是另一个类型的ParallelFor job;它是专门为了Transforms上的操作设计的。

注意:ParallelForTransform job是Unity中所有实现了IJobParallelForTransform接口的结构的总称。

C# Job System建议和故障排除

C# Job System tips and troubleshooting​docs.unity3d.com

当你使用Unity C# Job System时,确保你遵守以下几点:C# Job System tips and troubleshooting当你使用Unity C# Job System时,确保你遵守以下几点:

不要从一个job中访问静态的数据

在所有的安全性系统中你都应当避免从一个job中访问静态数据。如果你访问了错误的数据,你可能会使Unity崩溃,通常是以意想不到的方式。举例来说,访问一个MonoBehaviour可以导致域重新加载时崩溃。

注意:因为这个风险,未来版本的Unity会通过静态分析来阻止全局变量在job中的访问。如果你确实在job中访问了静态数据,你应当预见到你的代码会在Unity未来的版本中报错。

刷新已调度的批次

当你希望你的job开始执行时,你可以通过JobHandle.ScheduleBatchedJobs来刷新已调度的批次。注意调用这个接口时会对性能产生负面的影响。不刷新批次将会延迟调度job,直到主线程开始等待job的结果。在任何其他情况中,你应当调用JobHandle.Complete来开始执行过程。

注意:在Entity Component System(ECS)中批次会暗中为你进行刷新,所以调用JobHandle.ScheduleBatchedJobs是不必要的。

不要试图去更新NativeContainer的内容

由于缺乏引用返回值,不可能去直接修改一个NativeContainer的内容。例如,nativeArray[0]++ ;和 var temp = nativeArray[0]; temp++;一样,都没有更新nativeArray中的值。

你必须从一个index将数据拷贝到一个局部临时副本,修改这个副本,并将它保存回去,像这样:

MyStruct temp = myNativeArray[i];
temp.memberVariable = 0;
myNativeArray[i] = temp;

调用JobHandle.Complete来重新获得归属权

在主线程重新使用数据前,追踪数据的所有权需要依赖项都完成。只检查JobHandle.IsCompleted是不够的。你必须调用JobHandle.Complete来在主线程中重新获取NaitveContainer类型的所有权。调用Complete同时会清理安全性系统中的状态。不这样做的话会造成内存泄漏。这个过程也在你每一帧都调度依赖于上一帧job的新job时被采用。

在主线程中调用Schedule和Complete

你只能在主线程中调用ScheduleComplete方法。如果一个job需要依赖于另一个,使用JobHandle来处理依赖关系而不是尝试在job中调度新的job。

在正确的时间调用Schedule和Complete

一旦你拥有了一个job所需的数据,尽可能快地在job上调用Schedule,在你需要它的执行结果之前不要调用Complete。一个良好的实践是调度一个你不需要等待的job,同时它不会与当前正在运行的其他job产生竞争。举例来说,如果你在一帧结束和下一帧开始之前拥有一段没有其他job在运行的时间,并且可以接受一帧的延迟,你可以在一帧结束的时候调度一个job,在下一帧中使用它的结果。或者,如果这个转换时间已经被其他job占满了,但是在一帧中有一大段未充分利用的时段,在这里调度你的job会更有效率。

将NativeContainer标记为只读的

记住job在默认情况下拥有NativeContainer的读写权限。在合适的NativeContainer上使用[ReadOnly]属性可以提升性能。

检查数据的依赖

在Unity的Profiler窗口中,主线程中的"WaitForJobGroup"标签表明了Unity在等待一个工人线程上的job结束。这个标签可能意味着你以某种方式引入了一个资源依赖,你需要去解决它。查找JobHandle.Complete来追踪你在什么地方有资源依赖,导致主线程必须等待。

调试job

job拥有一个Run方法,你可以用它来替代Schedule从而让主线程立刻执行这个job。你可以使用它来达到调试目的。

不要在job中开辟托管内存

在job中开辟托管内存会难以置信得慢,并且这个job不能利用Unity的Burst编译器来提升性能。Burst是一个新的基于LLVM的后端编译器技术,它会使事情对于你更加简单。它获取C# job并利用你平台的特定功能产生高度优化的机器码。

更多信息

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值