要创建并成功运行作业,您必须执行以下操作
要在 Unity 中创建作业,请实现 IJob
接口。您可以使用 IJob
实现来安排一个在并行运行的任何其他作业中运行的单个作业。
IJob
具有一个必需方法:Execute
,Unity 在 工作线程 运行作业时会调用该方法。
在创建作业时,您还可以为其创建一个 JobHandle
,其他方法需要使用该方法来引用该作业。
重要:无法防止访问作业内的非只读或 可变您可以更改可变包的内容。这与不可变相反。只有本地包和嵌入式包是可变的。
请参阅 术语表 静态数据。访问此类数据会绕过所有安全系统,并可能导致应用程序或 Unity 编辑器崩溃。
当 Unity 运行时,作业系统会创建已安排作业数据的副本,这可以防止多个线程读取或写入相同数据。只有写入 NativeContainer
的数据才能在作业完成之后访问。这是因为作业使用的 NativeContainer
副本和原始 NativeContainer
对象都指向相同的内存。有关详细信息,请参阅有关 线程安全类型 的文档。
当作业系统从其作业队列中获取作业时,它会在单个线程上运行一次 Execute
方法。通常,作业系统会在后台线程上运行作业,但如果主线程变为空闲,它可以选择主线程。因此,您应该设计您的作业,使其在一帧内完成。
要安排作业,请调用 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);