译自官方手册,简述 Unity 另外一个多线程实现方式,JobSystem,为 Unity ECS 系统实现的根本html
原文git
JobSystem 管理一组多核中的工做线程(Work Thread),为避免上下文切换一般一个逻辑核配一个工做线程github
JobSystem 持有一个 Job 队列,工做线程从该队列中获取 Job 执行c#
Job 是执行特定任务的小工做单元,Job 能够互相依赖缓存
JobSystem 执行时复制而非引用数据,避免了数据竞争,但 JobSystem 只能使用memcpy
复制 blittable types 数据。Blittable types
是 .Net 框架中的数据类型,该类型数据在托管代码与原生代码间传递无需转换安全
复制数据来保证线程安全的弊端就是任务的结果也是独立的,所以使用NativeContainer
将结果储存在公共内存中多线程
NativeContainer
以相对安全的托管类型的方式指向一个非托管的内存地址,使Job 能够直接访问主线程数据而非复制框架
Unity 自带 NativeContainer
类型为 NativeArray
,ECS 包又扩展了NativeList
、NativeHashMap
、NativeMultiHashMap
和NativeQueue
svg
默认状况下,Job 同时拥有NativeContainer
的读写权限,但 C# Job System 不容许多个 Job 同时拥有对一个NativeContainer
的写权限,所以对不须要写权限的NativeContainer
加上[ReadOnly]
特性,以减小性能影响post
[ReadOnly]
public NativeArray<int> input;
复制代码
JobSystem 支持多个 Job 同时读取同一数据
根据 Job 执行时长决定使用哪一种 Allocator
Allocator.Temp
最快的分配方法,适用于一帧或几帧的生命时长,不能将该类型分配的数据传给 Job,在方法 Return 前执行Dispose
Allocator.TempJob
分配速度比 Temp 慢比 Persistent 快,4帧的生命时长且线程安全。若四帧内没有调用Dispose
,控制台会打印原生代码生成的警告。大部分小任务都使用该类型分配NativeContainer
Allocator.Persistent
是对malloc
的包装,可以维持尽量地生命时长,性能不足的状况下不该使用
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
复制代码
blittable types
或 NativeContainer
类型的成员变量Execute
方法当 Job 执行时,Execute
在一个核上执行一次
public struct MyJob : IJob
{
public float a;
public float b;
public NativeArray<float> result;
public void Execute()
{
result[0] = a + b;
}
}
复制代码
Schedule
方法只能在主线程调用Schedule
方法,将 Job 放入队列等待执行,一旦 Job 被调度进队列旧没法中断
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
// 填充数据
MyJob jobData = new MyJob();
jobData.a = 10;
jobData.b = 10;
jobData.result = result;
// 调度 Job
JobHandle handle = jobData.Schedule();
// 等待完成
handle.Complete();
// 全部 NativeArray 指向同一内存,可外部访问
float aPlusB = result[0];
// 释放 result array
result.Dispose();
复制代码
JobHandle 是在调用Schedule
返回的句柄,可以使用该句柄做为参数传入另外一个 Job 的Schedule
做为依赖,使后者等待前者执行完成再执行
JobHandle firstJobHandle = firstJob.Schedule();
secondJob.Schedule(firstJobHandle);
复制代码
对于多个依赖的 Job 可以使用JobHandle.CombineDependencies
组合这些 JobHandle
NativeArray<JobHandle> handles = new NativeArray<JobHandle>(numJobs, Allocator.TempJob);
// 填充 handles
JobHandle jh = JobHandle.CombineDependencies(handles);
复制代码
调用 JobHandle 的Complete
方法可以使主线程等待任务执行完成以安全访问该 Job 使用的 NativeContainer
,该方法会从内存中刷新 Job 并开始执行而后将该 Job 中的NativeContainer
持有权返回主线程
若不需访问数据,但须要当即刷新执行 Job 缓存,则能够使用JobHandle.ScheduleBatchedJobs
,但会影响性能
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;
}
}
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();
复制代码
对于IJob
,同一时间一个 Job 只能执行一个任务,若想同一时间执行多个相同的任务,则能够使用IJobParallelFor
一种使用场景为 ParallelFor Job 在多核上同时对同一 NativeArray 进行操做,每核仅负责部分工做,ParallelFor Job 的Execute
方法会传入index,用于访问数据源
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 Job 时需规定调度任务总长度和每批次长度,C# Job System 会根据批次长度将任务总长分批,再放入 Unity Job 队列,每批同步执行,每批次任务内仅执行一个 Job
当一个 Native Job 先完成时,它会“窃取”其余 Job 的一半批任务,既优化了性能,又保证了内存访问局部性
批次数越低,线程间的任务分配越均匀,但也会带来额外开销,所以须要逐一测试出最佳性能的批次数
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;
// Schedule the job with one Execute per index in the results array and only 1 item per processing batch
JobHandle handle = jobData.Schedule(result.Length, 1);
// Wait for the job to complete
handle.Complete();
// Free the memory allocated by the arrays
a.Dispose();
b.Dispose();
result.Dispose();
复制代码
专门用于操做 Transform 的 Parallel Job
不要使用 Job 访问静态数据
从 Job 访问静态数据会绕开全部安全系统,可能会致使 Unity 崩溃
使用 JobHandle.ScheduleBatchedJobs 方法当即执行已调度的 Job
Job 在被调度后会被缓存不会当即执行,该方法可当即清空缓存队列中的 Job 并执行,但会影响性能,或调用 JobHandle.Complete 执行,ECS 系统已经隐式清空了缓存,以你无需主动调用
不要更新 NativeContainer 内容
因为ref returns
的缺陷,没法直接修改 NativeContainer 中的内容,需按以下方式
MyStruct temp = myNativeArray[i];
temp.memberVariable = 0;
myNativeArray[i] = temp;
复制代码
调用 JobHandle.Complete 重获全部权
在主线程或新的 Job 使用前一 Job 占用的NativeContainer
数据前,必须调用JobHandle.Complete
从新获取其全部权,该方法会清空安全机制的状态,不然会致使内存泄漏( 不能仅查看JobHandle.IsCompleted状态)
只能在主线程调用 Schedule 和 Complete 方法
这两种方法只能在主线程调用,若一个 Job 依赖于另外一个 Job,则在主线程使用 JobHandle
Schedule 和 Complete 的正确时机
准备数据完成时便可调用 Schedule,仅当须要结果时才调用 Complete,如在一帧的结尾与下一帧开始的空档中调度一个 Job
使用 [ReadOnly] 标记 NativeContainer
Job 同时用于对 NativeContainer 的读写权限,使用 [ReadOnly] 标记只读 Job 中的 NativeContainer 可提高效率
检查数据依赖
在 Profiler 窗口中,主线程上的 WaitForJobGroup
标记代表 Unity 在等待一个工做线程的任务完成,该标记可能意味着须要解决的数据依赖,可经过查找JobHandle.Complete
找到这些依赖
Debugging jobs
能够调用Run
方法取代Schedule
在主线程执行 Job
不要在 Job 中分配托管内存
在 Job 中分配托管内存会很是慢,且没法使用 Burst 编译提高效率