C# Job System 是 Unity 从 2018 开始提供给用户的一个非常强大的功能,它允许用户以一种低成本的方式书写高效的多线程代码。
Unity 中文课堂的官方免费课程《DOTS 课程系列 | C# Job System 精要》将从以下方面讲解 C# Job System,点击阅读原文,即可解锁完整教程。
什么是 C# Job System?
IJobFor
Thread Local
Pointers & InterLocked
Batches & False sharing
Custom batch & Kick jobs
SoA vs AoS
什么是C# Job System?
C# Job System 是 Unity 从 2018 开始提供给用户的一个非常强大的功能,它允许用户以一种低成本的方式书写高效的多线程代码。我们先通过一个 Demo 来一步一步揭开 C# Job System 的面纱:
首先我们先来定义一个 Job:
public struct AddJob : IJobpublic float a;public float b;public NativeArray result;
public void Execute()result[0] = a + b;我们最先能观察到的就是 AddJob 实现了 IJob 接口,IJob可以让我们调度(Schedule)一个在单一工作(worker)线程里执行的任务。其次我们可以看到 AddJob 是一个 struct。
我们再来看一下 AddJob 里的变量部分:
public float a;public float b;public NativeArray result;Job 中的变量我们仅可以使用blittable types或者 Unity 为我们提供的NativeContainer容器,比如引擎内置的 NativeArray 或者 com.unity.collections package 中提供的容器类。
blittable types:
https://en.wikipedia.org/wiki/Blittable_types
NativeContainer:
https://docs.unity3d.com/Manual/JobSystemNativeContainer.html
注:为什么只能使用 blittable types?这是因为 C# Job System 使用了 Unity 内部的 native job system,C# Job System 会与 native job system 共享工作线程。为了达到这个目的,C# 中的 Job 数据需要被拷贝到 native 层来运行计算代码,blittable types 在这个拷贝过程中不需要做数据转换,因此 blittable types 在这里是必须的。不仅如此 blittable types 还有着其他的好处,我们会在后面的例子中看到。
让我们来总结一下声明一个 Job 的要点:
创建一个实现了 IJob 接口的 struct。
在 struct 中声明 blittable types 或者 NativeContainer 的变量。
在 Execute() 方法中实现 Job 的逻辑。
好,通过上面几步我们就成功创建了我们的 AddJob 。接下来我们来看一下如何调度(Schedule)一个 Job 以及如何获得 Job 执行后的结果:
var job = new AddJoba = 1,b = 2,result = result
var handle = job.Schedule();handle.Complete();Debug.Log($"result = {result[0]}");调度(Schedule)一个 Job是比较简单的,只需要调用 Schedule() 方法就可以了。这里比较有意思的是 Complete() 方法,在我们需要读取执行结果之前需要调用 Complete() 方法。但是 Complete() 不一定在 Schedule() 之后立即调用,也不一定在当前帧必须调用,也就是说一个 Job 本身不受 Update() 限制可以跨帧运行。当一个 Job 需要跨帧运行的时候,我们需要使用 IsCompleted 属性来判断 Job 是否执行完毕。
private void Update()if (handle.IsCompleted)handle.Complete();Debug.Log($"result = {result[0]}");注:即使 IsCompleted 返回 true,也必须要调用 Complete() 方法。具体可以参考 C# Job System tips and troubleshooting。
C# Job System tips and troubleshooting:
https://docs.unity3d.com/Manual/JobSystemTroubleshooting.html
这样我们就实现了了一个最简单的 Job,这里我给出完整的 Demo 代码,方便大家进一步理解上面介绍的内容:
public class AddJobBehaviour : MonoBehaviourpublic bool longRunningJob;private JobHandle handle;private NativeArray result;
public struct AddJob : IJobpublic float a;public float b;public NativeArray result;
public void Execute()result[0] = a + b;
private void Start()result = new NativeArray(1, Allocator.Persistent);
var job = new AddJoba = 1,b = 2,result = result
handle = job.Schedule();
if (!longRunningJob)handle.Complete();Debug.Log($"result = {result[0]}");
private void Update()if (handle.IsCompleted)handle.Complete();Debug.Log($"result = {result[0]}");
private void OnDestroy()if (result.IsCreated)result.Dispose();在上面的完整 Demo 代码中,有一点是之前没有提到的,就是下面这两句:
result = new NativeArray(1, Allocator.Persistent);
result.Dispose();这里大家可以很明显的注意到,NativeContainer 是需要显式管理内存的。关于这方面的内容我会在后面的 NativeContainer 章节继续跟大家聊。
好,到这里大家应该对 C# Job System 有了一个初步的了解。让我们来做一个小测验,看我们是否真的理解了上面的内容。
下面的代码,输出结果会是什么?
public class MyCounterJobBehaviour : MonoBehaviourpublic struct CounterJob : IJobpublic NativeArray numbers;public int result;
public void Execute()for (int i = 0; i < numbers.Length; i++)result += numbers[i];
// Start is called before the first frame updatevoid Start()var numCount = 10;NativeArray numbers = new NativeArray(numCount, Allocator.TempJob);
for (int i = 0; i < numCount; i++)numbers[i] = i + 1;
var jobData = new CounterJobnumbers = numbers,result = 0
var handle = jobData.Schedule();handle.Complete();Debug.Log($"result = {jobData.result}");numbers.Dispose();答案是result = 0
这个结果跟你想的一样么?
让我们来思考一下为什么是这个结果。我们再来看回顾一下 Job 的特点:
需要声明成 struct
struct 中的数据必须是 blittable 的或者是 NativeContainer
要实现 IJob 接口
这些限制条件其实都是为了一个目的,就是要把 C# 中的 Job 数据复制到 native 层,最终由 native job system 去执行 job 中的逻辑。想到这其实我们的答案也就显而易见了,Execute() 方法中修改的其实只是我们 CounterJob 的一个副本,并不是原始的 CounterJob。因此当我们需要从 Job 中获得计算结果的时候,我们需要使用 NativeContainer,否则会得到不正确的结果。下面是正确的写法:
using Unity.Collections;using Unity.Jobs;using UnityEngine;
public class CounterJobBehaviour : MonoBehaviourpublic struct CounterJob : IJobpublic NativeArray numbers;public NativeArray result;
public void Execute()var tmp = 0;for (int i = 0; i < numbers.Length; i++)tmp += numbers[i];
result[0] = tmp;
void Start()var numCount = 10;NativeArray numbers = new NativeArray(numCount, Allocator.TempJob);var result = new NativeArray(1, Allocator.TempJob);
for (int i = 0; i < numCount; i++)numbers[i] = i + 1;
var jobData = new CounterJobnumbers = numbers,result = result
var handle = jobData.Schedule();handle.Complete();
Debug.Log($"result = {result[0]}");
result.Dispose();numbers.Dispose();到这里我们就已经把 C# Job System 以及 IJob 大概了解了一下,相信大家应该已经注意到了,IJob 只能跑在单一工作(Worker)线程上,如果想要利用全部的工作(Worker)线程就需要用到我们下一节要介绍的另外一个接口了,那就是 IJobFor。
好,以上就是本节所有的内容了,下一节我们讲继续讨论 Job 的另一种形式:IJobFor。
感谢大家的耐心阅读
Unity 官方微信
第一时间了解Unity引擎动向,学习最新开发技巧
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.