1.定义一个Job结构体
首先,需要定义一个struct继承接口IJob。
注意:Job数据只接受引用类型的数据,无法使用类(calss)的数据。
public struct MyJob : IJob
{
public float a;
public float b;
public NativeArray<float> result;
public void Execute()
{
result[0] = a + b;
}
}
在这个结构体MyJob中进行需要进行的计算并将结果存储于NativeContainer中,Unity附带一个NativeContainer名为NativeArray的程序。
您还可以使用NativeSlice操作一个NativeArray来获取NativeArray从指定位置到指定长度的子集。
注意:实体组件系统(ECS)包扩展了Unity.Collections命名空间以包括其他类型的NativeContainer:
- NativeList- 可调整大小的NativeArray。
- NativeHashMap - 键值对。
- NativeMultiHashMap - 每个键有多个值。
- NativeQueue- 先进先出(FIFO)队列。
2.实例化Job并填充数据
// Create a native array of a single float to store the result. This example waits for the
// job to complete for illustration purposes
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
// Set up the job data
MyJob jobData = new MyJob();
jobData.a = 10;
jobData.b = 10;
jobData.result = result;
实例化Job后需要对结构体中的数据赋值,并对要保存结果的NativeContainer分配内存。
当创建 NativeContainer时,必须指定所需的内存分配类型。分配类型取决于Job运行的时间长度。通过这种方式,您可以定制分配以在每种情况下获得最佳性能。
NativeContainer内存分配和释放有三种分配器类型。在实例化你的NativeContainer时候需要指定合适的一个类型。
- Allocator.Temp分配的时候最快。它适用于寿命为一帧或更少的分配。您不应该使用Temp将NativeContainer分配传递给Jobs。您还需要在从方法(例如MonoBehaviour.Update,或从本机代码到托管代码的任何其他回调)调用返回之前调用该方法Dispose()。
- Allocator.TempJob是一个比Temp慢的分配,但速度比Persistent快。它适用于四帧生命周期内的分配,并且是线程安全的。如果在四个帧内没有调用Dispose,则控制台会打印一个从本机代码生成的警告。大多数小型Jobs都使用这个NativeContainer分配类型。
- Allocator.Persistent是最慢的分配,只要你需要它,就一直存在。并且如果有必要的话,可以持续整个应用程序的生命周期。它是直接调用malloc的包装器。较长的Jobs可以使用此NativeContainer分配类型。你不应该使用Persistent在性能至关重要的地方使用。
例如:
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
注意:上例中的数字1表示NativeArray的大小。在这种情况下,它只有一个数组元素(因为它只存储一个数据result)。
3.调用Schedule方法
// Schedule the job
JobHandle handle = jobData.Schedule();
// Wait for the job to complete
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将Job放入Job队列中以便在适当的时间执行。一旦调度,你就不能打断Job的运行。
注意:您只能在主线程调用Schedule。
最终的结果需要从结构体的NativeContainer中读取出来。请及时使用Dispose释放内存。
4.Job依赖
如果需要使用多个Job,且需要用到前一个或几个Job的数据,则需要使用到Job依赖。
当您调用Job的Schedule方法时,它将返回JobHandle。您可以在代码中使用JobHandle 作为其他Job的依赖关系。如果Job取决于另一个Job的结果,您可以将第一个作业JobHandle作为参数传递给第二个作业的Schedule方法
JobHandle firstJobHandle = firstJob.Schedule();
secondJob.Schedule(firstJobHandle);
如果Job有许多依赖项,则可以使用JobHandle.CombineDependencies方法合并它们。CombineDependencies允许您将它们传递给Schedule方法。
NativeArray<JobHandle> handles = new NativeArray<JobHandle>(numJobs, Allocator.TempJob);
// Populate `handles` with `JobHandles` from multiple scheduled jobs...
JobHandle jh = JobHandle.CombineDependencies(handles);
Job Code:
// 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;
}
}
// Job adding one to a value
public struct AddOneJob : IJob
{
public NativeArray<float> result;
public void Execute()
{
result[0] = result[0] + 1;
}
}
Main Code:
// Create a native array of a single float to store the result in. This example waits for the job to complete
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
// Setup the data for job #1
MyJob jobData = new MyJob();
jobData.a = 10;
jobData.b = 10;
jobData.result = result;
// Schedule job #1
JobHandle firstHandle = jobData.Schedule();
// Setup the data for job #2
AddOneJob incJobData = new AddOneJob();
incJobData.result = result;
// Schedule job #2
JobHandle secondHandle = incJobData.Schedule(firstHandle);
// Wait for job #2 to complete
secondHandle.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();
接口:
定义Job的结构体时,有3种不同的接口用于不同计算:
IJob,IJobParallelFor,IJobParallelForTransform
IJob:
允许调度一个与主线程和其他Jobs并行的单独的Job。当Job被列入工作计划时,Job的Execute方法将在一个工作线程上调用。返回的Job句柄可用于确保Job已完成。或者可以将其作为依赖项传递给其他Job,从而确保Job在工作线程上一个接一个地执行。
IJobParallelFor:
允许对本机NativeContainer的每个元素或固定次数的迭代执行相同的独立操作。当计划作业时,Execute(int index)方法将在多个工作线程上并行调用。Execute(int index)将对从0到指定的长度的每个索引执行一次。每个迭代必须独立于其他迭代(安全系统为您执行此规则)。索引没有保证顺序,而是在多个核上并行执行。
IJobParallelForTransform:
Unity的文档中没有详细介绍,只写了名字
在从Package Manager中加载了Jobs包后,Unity增加了三个新的接口IJobParallelForBatch、IJobParallelForFilter和IJobParallelForDefer,不过它们现在是不安全的(Unity说的,等待以后完善)。
IJobParallelForBatch:
它的工作原理类似于IJobParallelFor,但是它不是为每个索引调用一次执行函数,而是每个执行批次调用一次执行函数。如果您需要同时处理多个项目,但仍然希望并行处理,那么这是非常有用的。此Job类型的常见场景是,如果需要创建一个临时数组,并且希望避免一次创建数组中的每个项。通过使用IJobParallelFor,您可以为每个批创建一个临时数组。
IJobParallelForFilter和IJobParallelForDefer还没有具体文档。