Unity3D :创建并运行作业
推荐:将NSDT场景编辑器加入你的3D工具链
3D工具集:NSDT简石数字孪生
创建并运行作业
要创建并成功运行作业,必须执行以下操作:
- 创建作业:实现
IJob
接口。 - 计划作业:在作业上调用
计划
方法。 - 等待作业完成:如果作业已完成,它会立即返回,当您想要访问数据时,可以在作业上调用
Complete
方法。
创建作业
要在 Unity 中创建作业,请实现 IJob
接口。您可以使用您的实现来计划与正在运行的任何其他作业并行运行的单个作业。IJob
IJob
有一个必需的方法:执行
,每当工作线程运行作业时,Unity 都会调用该方法。
创建作业时,还可以为其创建 JobHandle
,其他方法需要使用该作业来引用作业。
重要:没有针对访问非只读或可变
作业中的静态数据。访问此类数据会绕过所有安全系统,并可能导致应用程序或 Unity 编辑器崩溃。
当 Unity 运行时,作业系统会复制计划的作业数据,从而防止多个线程读取或写入相同的数据。作业完成后,只能访问写入 的数据。这是因为作业使用的 副本和原始对象都指向同一内存。有关详细信息,请参阅有关线程安全类型的文档。NativeContainerNativeContainerNativeContainer
当作业系统从其作业队列中选取作业时,它会在单个线程上运行该方法一次。通常,作业系统在后台线程上运行作业,但如果主线程变为空闲,它可以选择主线程。因此,您应该将作业设计为在框架下完成。Execute
计划作业
要安排作业,请调用 。这会将作业放入作业队列中,作业系统在其所有依赖项(如果有)完成后开始执行作业。计划后,您将无法中断作业。您只能从主线程调用。ScheduleSchedule
提示:作业有一个 Run
方法,您可以使用该方法代替 在主线程上立即执行作业。您可以使用它进行调试。Schedule
完成作业
调用作业并且作业系统执行作业后,可以调用 上的 Complete
方法来访问作业中的数据。最佳做法是在代码中尽可能晚地调用。调用 时,主线程可以安全地访问作业正在使用的 NativeContainer
实例。调用还可以清理安全系统中的状态。不这样做会导致内存泄漏。ScheduleJobHandleCompleteCompleteComplete
工作示例
下面是将两个浮点值相加的作业示例。它实现 IJob
,使用 a 获取作业的结果,并使用 Execute
方法实现其中的作业:NativeArray
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();
}
}
安排和完成最佳做法
最佳做法是在获得所需数据后立即调用作业,在需要结果之前不要调用作业。ScheduleComplete
您可以将不太重要的作业安排在框架的一部分中,这样它们就不会与更重要的工作竞争。
例如,如果一帧结束和下一帧开始之间有一段时间段没有作业正在运行,并且一帧延迟是可以接受的,则可以将作业安排在帧结束时,并在下一帧中使用其结果。或者,如果您的应用程序与其他作业的转换期饱和,并且帧中的其他位置存在未充分利用的时间段,则改为在那里安排作业更有效。
您还可以使用分析器
以查看 Unity 在何处等待作业完成。主线程上的标记指示了这一点。此标记可能意味着已在应解决的某处引入了数据依赖项。查找以跟踪强制主线程等待的数据依赖项的位置。WaitForJobGroupIDJobHandle.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);
由3D建模学习工作室整理翻译,转载请注明出处!