在调度作业时,只能有一个作业正在执行一项任务。在游戏中,通常希望对大量对象执行相同的操作。有一个称为 IJobParallelFor 的单独作业类型可以处理此问题。
注意:“ParallelFor”作业是 Unity 中对于任何实现 IJobParallelFor
接口的结构的统称。
ParallelFor 作业使用一个数据 NativeArray 作为其数据源。ParallelFor 作业在多个核心上运行。每个核心有一个作业,每个作业处理一部分工作量。IJobParallelFor
的行为类似于 IJob
,但其并非调用单个 Execute 方法,而是对数据源中的每一项都调用一次 Execute
方法。Execute
方法中有一个整数参数。该索引用于访问和操作作业实现中的数据源的单个元素。
struct IncrementByDeltaTimeJob: IJobParallelFor
{
public NativeArray<float> values;
public float deltaTime;
public void Execute (int index)
{
float temp = values[index];
temp += deltaTime;
values[index] = temp;
}
}
在调度 ParallelFor 作业时,必须指定要拆分的 NativeArray
数据源的长度。如果结构中有多个 NativeArray
,Unity C# 作业系统无法知道您希望将哪个用作数据源。长度还告诉 C# 作业系统应该有多少个 Execute
方法。
在后台,ParallelFor 作业的调度更加复杂。在调度 ParallelFor 作业时,C# 作业系统将工作分成多个批次以便在多个核心之间分配任务。每个批次包含一小部分 Execute
方法。然后,针对每个 CPU 核心,C# 作业系统会在 Unity 本机作业系统中调度最多一个作业,并向该本机作业传递一些需要完成的批次。
一个本机作业在其他作业之前完成自己负责的批次时,它会从其他本机作业窃取剩余批次。它一次只能窃取本机作业剩余批次的一半,以确保缓存局部性。
要优化该过程,必须指定批次计数。批次计数可以控制您获得的作业数量,以及线程之间重新分配工作的细化程度。批次计数较低(例如 1)可以使线程之间的工作分布更均匀。这样确实会带来一些开销,所以有时候增加批次计数会更好。一种有效的策略是从 1 开始并增加批次计数直到性能增益可忽略不计。
作业代码:
// 将两个浮点值相加的作业
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();