要创建并成功运行作业,您必须执行以下步骤:
要在 Unity 中创建作业,请实现 IJob 接口。使用 IJob 的实现即可调度与正在运行的其他作业并行运行的单个作业。
IJob 拥有一个必需的方法 Execute。只要工作线程运行作业,Unity 就会调用此方法。
创建作业时,还可以为其创建 JobHandle,以供其他方法引用该作业时使用。
重要信息:没有任何保护措施可以防止从作业内部访问非只读或可变静态数据。访问此类数据会绕过所有安全系统,并可能导致应用程序或 Unity 编辑器崩溃。
Unity 运行时,作业系统会创建被调度作业数据的副本,从而防止会有一个以上的线程读取或写入相同的数据。作业完成后,只能访问写入 NativeContainer 的数据。这是因为作业使用的 NativeContainer 副本和原始 NativeContainer 对象都指向同一内存。详情请参阅线程安全类型的文档。
当作业系统从作业队列中获取作业时,会在单个线程上运行一次 Execute 方法。作业系统通常会在后台线程上运行作业,但当主线程空闲时,也可以选择主线程。因此,您应该将作业设计为在 1 帧内完成。
要调度作业,请调用 Schedule。这会将作业放入作业队列中,作业系统会在作业的所有依赖项(如果有)完成后开始执行作业。一旦作业被调度,就不能中断作业。只能从主线程调用 Schedule。
提示:作业有一个 Run 方法,可以使用此方法代替 Schedule 在主线程上立即执行作业。可将此方法用于调试。
调用 Schedule 且作业系统执行作业后,即可对 JobHandle 调用 Complete 方法以访问作业中的数据。最佳做法是在代码中尽可能晚调用 Complete。调用 Complete 时,主线程可以安全访问作业之前所使用的 NativeContainer 实例。调用 Complete 也会清除安全系统中的状态。不这样做会引发内存泄漏。
下方是将两个浮点值相加的作业示例。实现了 IJob,使用 NativeArray 获取了作业的结果,并对其中的作业实现使用 Execute 方法:
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
// Job adding two floating point values together
public struct MyJob : IJob
{
public float a;
public float b;
public NativeArray<float> result;
public void Execute()
{
result[0] = a + b;
}
}
以下示例以 MyJob 作业为基础在主线程上调度作业:
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
public class MyScheduledJob : MonoBehaviour
{
// Create a native array of a single float to store the result. Using a
// NativeArray is the only way you can get the results of the job, whether
// you're getting one value or an array of values.
NativeArray<float> result;
// Create a JobHandle for the job
JobHandle handle;
// Set up the job
public struct MyJob : IJob
{
public float a;
public float b;
public NativeArray<float> result;
public void Execute()
{
result[0] = a + b;
}
}
// Update is called once per frame
void Update()
{
// Set up the job data
result = new NativeArray<float>(1, Allocator.TempJob);
MyJob jobData = new MyJob
{
a = 10,
b = 10,
result = result
};
// Schedule the job
handle = jobData.Schedule();
}
private void LateUpdate()
{
// Sometime later in the frame, wait for the job to complete before accessing the results.
handle.Complete();
// All copies of the NativeArray point to the same memory, you can access the result in "your" copy of the NativeArray
// float aPlusB = result[0];
// Free the memory allocated by the result array
result.Dispose();
}
}
最好在拿到作业所需的数据后就立即对作业调用 Schedule,并仅在需要结果时才对作业调用 Complete。
您可以将不太重要的作业安排在帧的某个部分,从而让它们不与更重要的作业发生竞争。
例如,如果在帧末尾和下一帧开头之间的一段时间内不存在作业运行,并且这时可以接受一帧的延迟,那么您可以将作业调度至该帧结尾处,并在下一帧使用其结果。或者,如果您的应用程序在该转换期内安排满了其他作业,且该帧的其他地方存在未充分利用的时间段,那么将作业调度至该时段会更为高效。
您还可以使用性能分析器查看 Unity 等待作业完成的位置。主线程上的标记 WaitForJobGroupID 表示了位置。此标记意味着您可能在某处引入了应处理的数据依赖项。请查找 JobHandle.Complete 来搜查让主线程强制等待的数据依赖项的位置。
与线程不同,作业不会产出执行。作业启动后,该作业的工作线程就会致力于完成该作业,之后才会运行其他作业。因此,最好将长时间运行的作业拆分为相互依赖的小型作业,而不是提交相对于系统中其他作业需要很长时间才能完成的作业。
作业系统通常运行多个作业依赖链,因此如果将长时间运行的任务拆分为多个部分,则多个作业链有可能会同时取得进展。相反的,如果作业系统中只有长时间运行的作业,那么这些作业可能会占用所有的工作线程,并阻止独立作业的执行。这可能会推后主线程明确等待的重要作业的完成时间,导致主线程出现原本不会出现的停滞。
特别是,长时间运行的 IJobParallelFor 作业会对作业系统产生负面影响,因为这些作业类型会故意尝试在作业批量大小允许的情况下,在尽可能多的工作线程上运行。如果无法拆分长时间的并行作业,请在调度作业时考虑增加作业的批次大小,以限制接受长时间运行作业的工作线程数量。
MyParallelJob jobData = new MyParallelJob();
jobData.Data = someData;
jobData.Result = someArray;
// Use half the available worker threads, clamped to a minimum of 1 worker thread
const int numBatches = Math.Max(1, JobsUtility.JobWorkerCount / 2);
const int totalItems = someArray.Length;
const int batchSize = totalItems / numBatches;
// Schedule the job with one Execute per index in the results array and batchSize items per processing batch
JobHandle handle = jobData.Schedule(result.Length, totalItems, batchSize);