1、C# Job System特点
(1)提供C# API,用户可以借助Unity C# Job System更简便的编写多线程代码。
(2)利用多线程处理数据提高性能。并且Unity将Burst编译器与C# Job System配合使用提高代码生成质量,还可以大大降低移动设备的电耗。
(3)与Unity的原生作业系统相集成。用户编写的代码与Unity共享工作线程
2、Job Sytem工作流
(1)每一个Job就是完成一项特定任务的一个工作单位,Job包含要处理的数据以及对数据的操作行为。
(2)作业系统将创建好的Job放到主线程的一个作业队列(Jobs queue)中。
(3)作业系统会跨多个核心管理一组工作线程,作业系统会从Jobs queue中提取job并根据job的数据大小和参数将job分发到不同的工作线程(worker thread)。
其中工作线程个数不超过CPU核心数,避免创建过多争用CPU资源。
3、C# Job
(1)Unity中的Job就是实现IJob接口的一个结构体(struct)。
(2)安全系统和Job中数据类型
Job中的成员变量数据类型只能为blittable类型或者NativeContainer。
Unity C#作业系统为了避免多线程竞争条件导致的错误,实现了一个安全系统,以复制数据的方式避免竞争条件,这就意味着作业只能访问blittable数据类型。但为了解决安全系统操作不同数据副本的限制,引入了NativeContainer类型。
NativeContainer是一种托管值类型,为本机内存提供了一个相对安全的C#封装器,包含一个指向非托管分配的指针。
安全系统内置于所有NativeContainer类型中并会跟踪在NativeContainer中读写的内容。
安全系统包含DisposeSentinel和AtomicSafetyHandle。
DisposeSentinel 可检测内存泄漏,如果未正确释放内存,则会报错。
使用AtomicSafetyHandle可以在代码中转移NativeContainer的所有权。保证不同作业可以顺序安全的写入相同的NativeContainer。安全系统运行多个作业并行读取相同数据。
4、Unity中作业系统应用
(1)单个作业
单个作业应用步骤如下:
创建作业 -> 实例化作业 -> 填充作业数据 -> 调用Schedule方法
只能从主线程调用schedule,调用Schedule会将该作业放入作业队列以便在适当的时间执行。一旦作业已调度,就不能中断。
示例如下:
// 将两个浮点值相加的作业
public struct MyJob : IJob
{
public float a;
public float b;
public NativeArray<float> result;
public void Execute()
{
result[0] = a + b;
}
}
// 创建单个浮点数的本机数组以存储结果。此示例等待作业完成,仅用于演示目的
NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);
// 设置作业数据
MyJob jobData = new MyJob();
jobData.a = 10;
jobData.b = 10;
jobData.result = result;
// 调度作业
JobHandle handle = jobData.Schedule();
// 等待作业完成
handle.Complete();
// NativeArray 的所有副本都指向同一内存,您可以在"您的"NativeArray 副本中访问结果
float aPlusB = result[0];
// 释放由结果数组分配的内存
result.Dispose();
(2)依赖作业
调用Schedule方法会返回一个作业JobHandle,如果作业Job2依赖作业Job1,则可以将JobHandle1作为参数传入Job2作业的Schedule方法。示例如下:
JobHandle firstJobHandle = firstJob.Schedule();
secondJob.Schedule(firstJobHandle);
如果一个作业有多个依赖项,则可以使用JobHandle.CombineDependencies方法合并所有依赖项,CombineDependencies可以将所有依赖项传递给Schedule方法。示例如下:
NativeArray<JobHandle> handles = new NativeArray<JobHandle>(numJobs, Allocator.TempJob);
// 使用来自多个调度作业的 `JobHandles` 填充 `handles`...
handles[0] = firstJobHandle;
handles[1] = secondJobHandle;
...
handles[numJobs - 1] =numJobsHandle;
JobHandle jh = JobHandle.CombineDependencies(handles);
(3)ParallelFor 作业
上述的Job只能一个作业执行一项任务,如果希望一个作业并行执行大量相同的操作可以继承IJobParallelFor接口创建ParallelFor作业。Parallel作业使用一个NativeArray作为数据源,数据源中的每一项都会调用一次Execute方法。如下图所示,C#作业系统会将ParallelFor作业分成多个批次,然后将不同的批次任务分配在不同的核心上。针对每个CPU核心,C#作业系统会在Unity本机作业系统中调度做多一个作业,并向该本机作业传递一些需要完成的批次。
示例如下:
// 将两个浮点值相加的作业
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;
// 调度作业,为结果数组中的每个索引执行一个 Execute 方法,且每个处理批次只处理一项
JobHandle handle = jobData.Schedule(result.Length, 1);
// 等待作业完成
handle.Complete();
// 释放数组分配的内存
a.Dispose();
b.Dispose();
result.Dispose();
另外Unity提供了一种特殊的ParallelFor作业:ParallelForTransform作业,专为操作变换组件设计,只需要实现IJobParallelForTransform接口。其中TransformAccessArray 是一个专门用于存储transform的NativeArrray,调用Schedule方法作为参数传给作业,在作业中使用TransformAccess 获取transform。
示例如下:
class TransformJobs:MonoBehaviour
{
//用于存储transform的NativeArray
private TransformAccessArray transformAccessArray;
private NativeArray<Vector3> velocities;
PositionUpdateJob job;
JobHandle positionJobHandle;
struct PositionUpdateJob : IJobParallelForTransform
{
public float dt;
//给每个物体设置一个速度
[ReadOnly]
public NativeArray<Vector3> velocity;
public void Execute(int i, TransformAccess transform)
{
transform.position += velocity[i] * dt;
}
}
private void Start()
{
velocities = new NativeArray<Vector3>(10000, Allocator.TempJob);
//生成一个球体,作为模板
var sphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
//保存transform的数组,用于生成transform的Native Array
var transforms = new Transform[10000];
for(int i =0;i<100;i++)
{
for(int j = 0;j<100;j++)
{
var go = GameObject.Instantiate(sphere);
go.transform.position = new Vector3(j, 0, i);
transforms[i * 100 + j] = go.transform;
velocities[i * 100 + j] = new Vector3(0.1f * j, 0, 0.1f * j);
}
}
transformAccessArray = new TransformAccessArray(transforms);
}
private void Update()
{
//实例化Job并传入数据
job = new PositionUpdateJob()
{
dt = Time.deltaTime,
velocity = velocities,
};
//调度Job
positionJobHandle = job.Schedule(transformAccessArray);
}
//保证当前帧内Job执行完毕
private void LateUpdate()
{
positionJobHandle.Complete();
}
//OnDestroy中释放NativeArray内存
private void OnDestroy()
{
velocities.Dispose();
transformAccessArray.Dispose();
}
}
5、C#作业系统使用注意事项:
(1)避免从作业访问静态数据
(2)NativeContainer标记为只读
默认情况下,作业对NativeContainer类型具有读写访问权限。对于不需要写入操作的NativeContainer使用[Readobly]标记为只读可以提高性能。
(3)不要尝试更新NativeContainer内容
由于缺少ref返回值,因此无法直接更改NativeContainer的内容。例如nativeArray[0]++和var temp = nativeArray[0];temp++;都不会更新nativeArray中的值。
正确的做法是拷贝一个副本,修改完后再赋值给nativearray,示例如下:
MyStruct temp = myNativeArray[i];
temp.memberVariable = 0;
myNativeArray[i] = temp;
(4)不要在作业中分配托管内存
在作业中分配托管内存非常慢,而且作业无法使用 Unity Burst 编译器来提高性能。Burst 是一种新的基于 LLVM 的后端编译器技术,可以简化您的工作。此编译器获取 C# 作业并生成高度优化的机器代码,从而利用目标平台的特定功能。
6、C# Job System与线程比较
(1)编码容易
(2)线程管理与安全
使用C# Job System无需用户自己考虑以下问题,Unity的安全系统会自动处理:
a、Resource contention/context switching
b、Race condition
(3)数据布局
cache friendly flat memory layout
(4)Work with Component
可以针对Unity中的组件操作,例如继承 IJobParallelForTransform 的作业,专为操作变换组件设计。
(5)Burst Compiler
对Job执行函数进行优化