Unity 的JobSystem允许创建多线程代码,以便应用程序可以使用所有可用的 CPU 内核来执行代码,这提供了更高的性能,因为您的应用程序可以更高效地使用运行它的所有 CPU 内核的容量,而不是在一个 CPU 内核上运行所有代码。
可以单独使用JobSystem,为了提高性能,可以和Burst 编译器一块使用,Burst 编译器改进了代码生成,从而提高了移动设备的性能并减少了电池消耗。
还可以将JobSystem与 Unity 的实体组件系统结合使用,以创建高性能的面向数据的代码。
概念:
单线程:一次执行一条指令,并且一次产生一个结果。加载和完成程序的时间取决于 CPU 需要完成的工作量。
多线程:利用了 CPU 在多个核心上同时处理多个线程的能力。这种情况下不是逐个执行任务或指令,而是同时运行任务或指令。
什么是多线程? - Unity 手册
JobSystem:通过创建Job而不是线程来管理多线程代码,将创建的Job放入Job队列中等待执行,工作线程从Job队列中获取并执行Job,用依赖关系来确保Job用适当的顺序执行。
Job:完成一项特定任务的一个小工作单位,会接收参数并对数据进行操作,可以是独立的,也可依赖其他作业完成之后才能运行。
Job依赖关系: 如果jobA依赖于jobB,系统将确保jobA不会在jobB完成之前开始执行。
什么是作业系统? - Unity 手册
优势:
1.多线程并行计算:利用多核CPU提升性能。
2.无GC分配:使用 Native 容器避免托管堆分配。
3.Burst 编译优化:生成高效原生代码(性能提升5-10倍)。
使用:
1.安装
通过Package Manager添加
通过Git URL地址搜索Job System包
com.unity.jobs:Job System 的核心功能包。
搜索到Jobs包后点击右下角Import
2.JobSystem使用入门:
1.NativeContainer:
1.核心容器:NativeArray<T> 、NativeSlice<T>
2.特殊数据结构容器:NativeBitArray
3.动态容器:NativeList<T>、NativeQueue<T>
4.并行安全容器:
NativeParallelHashMap<TKey,TValue>
NativeParallelHashSet<T>
NativeParallelMultiHashMap<TKey,TValue>
5.迭代器:
NativeParallelMultiHashMapIterator<T>
2.第一个Job例子:
1.创建Job:
1.创建实现 IJob 的结构。
2.添加该作业使用的成员变量(为 blittable 类型和 NativeContainer 类型之一)。
3.在结构中创建一个名为 Execute 的方法,并在其中实现该作业。
public struct MyJob : IJob
{
public float a;
public float b;
public NativeArray<Vector3> result;
public void Execute()
{
result[0] = new Vector3(a,0,b);
}
}
2.运行Job:
1.实例化该作业。
2.填充作业的数据。
3.调用 Schedule 方法,会将该作业放入作业队列中,以便在适当的时间执行。
NativeArray<Vector3> result;
JobHandle handle;
void Update()
{
result = new NativeArray<Vector3>(1, Allocator.TempJob);
MyJob jobData = new MyJob
{
a = 10,
b = 10,
result = result
};
handle = jobData.Schedule();
}
private void LateUpdate()
{
handle.Complete();
Debug.Log(result[0]);
result.Dispose();
}
结果:
3.JobHandle 和依赖项:
调用Job的 Schedule 方法时,将返回 JobHandle。可以在代码中使用 JobHandle 作为其他Job的依赖项,一个Job可以依赖于多个Job,在其所依赖的Job完成之前,JobSystem不会运行此Job。
JobHandle firstJobHandle = firstJob.Schedule();
secondJob.Schedule(firstJobHandle);
合并依赖项:使用JobHandle.CombineDependencies合并多个依赖项。
NativeArray<JobHandle> handles = new NativeArray<JobHandle>(numJobs, Allocator.TempJob);
JobHandle jh = JobHandle.CombineDependencies(handles);
多个依赖项执行例子:
1.在定义新的Job:
public struct MyJob : IJob
{
public float a;
public float b;
public NativeArray<Vector3> result;
public void Execute()
{
for (int i = 0; i< 100; i++)
{
a += 1;
b += 1;
}
result[0] = new Vector3(a + 100, 0, b + 100);
}
}
// 将一个值加一的作业
public struct AddOneJob : IJob
{
public NativeArray<Vector3> result;
public void Execute()
{
result[0] = result[0] + Vector3.one;
}
}
2.调度Job:
NativeArray<Vector3> result;
JobHandle handle;
JobHandle firstHandle;
void Update()
{
result = new NativeArray<Vector3>(1, Allocator.TempJob);
MyJob jobData = new MyJob
{
a = 10,
b = 10,
result = result
};
firstHandle = jobData.Schedule();
AddOneJob incJobData = new AddOneJob();
incJobData.result = result;
// 调度作业 #2
handle = incJobData.Schedule(firstHandle);
}
private void LateUpdate()
{
handle.Complete();
Debug.Log(result[0]);
result.Dispose();
}
结果:
4.并行Job:
在调度 ParallelFor 作业时,必须指定要拆分的 NativeArray 数据源的长度(表示有多少个 Execute 方法)。如果结构中有多个 NativeArray,无法确定将哪个用作数据源。
C# JobSystem将Job分成多个批次以便在多个核心之间分配任务,每个批次包含一小部分 Execute 方法,然后,JobSystem在 Unity 的Native Job System中为每个 CPU 内核安排一个Job,并将该Native Job传递给要完成的批次,如下图所示:
1.ParallelFor 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];
}
}
调度Job:
public class ParallelJobTest : MonoBehaviour
{
NativeArray<float> a;
NativeArray<float> b;
NativeArray<float> result;
JobHandle handle;
// Update is called once per frame
void Update()
{
a = new NativeArray<float>(3, Allocator.TempJob)
{
[0] = 1,
[1] = 2,
[2] = 3
};
b = new NativeArray<float>(3, Allocator.TempJob)
{
[0] = 100,
[1] = 200,
[2] = 111
};
result = new NativeArray<float>(3, Allocator.TempJob);
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
handle = jobData.Schedule(result.Length, 1);
// Wait for the job to complete
handle.Complete();
Debug.Log($"{result[0]},{result[1]},{result[2]}");
// Free the memory allocated by the arrays
a.Dispose();
b.Dispose();
result.Dispose();
}
}
结果:
2.ParallelForTransform Job:
ParallelForTransform Job允许对传递到作业中的所有转换的每个位置、旋转和缩放执行相同的独立作。
定义一个ParallelForTransform Job:
public struct VelocityJob : IJobParallelForTransform
{
[ReadOnly]
public NativeArray<Vector3> velocity;
public float deltaTime;
public void Execute(int index, TransformAccess transform)
{
var pos = transform.position;
pos += velocity[index] * deltaTime;
transform.position = pos;
}
}
调度Job:
public class ParallelForTransformTest : MonoBehaviour
{
[SerializeField] public Transform[] m_Transforms;
TransformAccessArray m_AccessArray;
// Start is called before the first frame update
void Start()
{
m_AccessArray = new TransformAccessArray(m_Transforms);
}
// Update is called once per frame
void Update()
{
var velocity = new NativeArray<Vector3>(m_Transforms.Length, Allocator.Persistent);
for (var i = 0; i < velocity.Length; ++i)
velocity[i] = new Vector3(0f, 10f, 0f);
var job = new VelocityJob()
{
deltaTime = Time.deltaTime,
velocity = velocity
};
JobHandle jobHandle = job.Schedule(m_AccessArray);
jobHandle.Complete();
Debug.Log(m_Transforms[0].position);
velocity.Dispose();
}
private void OnDestroy()
{
m_AccessArray.Dispose();
}
}
结果:
3.注意事项:
1.不要从Job访问静态数据。
2.不要更新 NativeContainer 内容。
3.调用 JobHandle.Complete 以重新获得所有权。
4.在适当时间使用 Schedule 和 Complete 方法。
5.将 NativeContainer 类型标记为只读。
6.不要在Job中分配托管内存。
参考链接:
ArtStation - Unity Job System in Practice. How we increased FPS from 15 to 70 in our game