学习目标:
学习使用JobSystem
学习内容:
JobSystem的基础概念 线程的知识 JobSystem的使用学习时间:
2022.1.25
学习产出:
搬运一下官方文档的详解,文章内容加上一些自己理解和解释以及易错点介绍,将其总结为学习笔记
里面有很多我在使用JobSystem中遇到的坑,以及原因
希望可以帮助你们快速入门
1.什么是JobSystem?我们为什么要学习它?
Unity C# Job System 允许用户编写与 Unity 的其余部分良好交互的多线程代码,并且更容易编写正确的代码。
编写多线程代码可以提供高性能优势。其中包括帧速率的显着提高。将 Burst 编译器与 C#
作业一起使用可以提高代码生成质量,这也可以显着减少移动设备上的电池消耗。
C# Job System 的一个重要方面是它与 Unity 内部使用的(Unity 的本地作业系统)集成。用户编写的代码和 Unity
共享工作线程。这种合作避免了创建的线程多于CPU 内核,从而导致 CPU 资源的争用。
默认情况下,脚本中几乎所有的执行语句都在Main thread上执行。这是一条通过CPU虚拟的路径,你可以将它想象为高速公路
同样的,我们可以将这条高速公路上的工作任务,分配给其他的高速公路。也就是创建Job来分担我们Mainthread上面的任务,减轻它的压力。我们可以将简单的而费时的计算分配给其他线程来做,减轻主线程的压力。这也是我们使用JobSystem的重要原因
Jobsystem的流程如下:
- 定义Job
- 实例化Job
- 执行Job
- 完成Job
2.什么是多线程?
在单线程计算系统中,一次输入一条指令,一次输出一个结果。加载和完成程序的时间取决于您需要 CPU 完成的工作量。
多线程是一种利用 CPU 跨多个内核同时处理多个线程的能力的编程类型。它们不是一个接一个地执行任务或指令,而是同时运行。
默认情况下,一个线程在程序开始时运行。这是“主线”。主线程创建新线程来处理任务。这些新线程彼此并行运行,并且通常在完成后将其结果与主线程同步。
如果您有一些需要长时间运行的任务,这种多线程方法会很有效。然而,游戏开发代码通常包含许多要一次执行的小指令。如果您为每个线程创建一个线程,您最终会得到许多线程,每个线程的生命周期都很短。这可能会突破CPU 和操作系统的处理能力极限。
可以通过拥有一个线程池来缓解线程生命周期的问题。但是,即使您使用线程池,您也很可能同时有大量线程处于活动状态。线程多于 CPU 内核会导致线程相互竞争 CPU资源,从而导致频繁的上下文切换。上下文切换是在执行过程中保存线程状态的过程,然后在另一个线程上工作,然后重建第一个线程,稍后继续处理它。上下文切换是资源密集型的,因此您应该尽可能避免使用它。
3.什么是JobSystem?
JobSystem通过创建作业而不是线程来管理多线程代码。
JobSystem管理一组跨多个内核的工作线程。它通常每个逻辑 CPU
核心有一个工作线程,以避免上下文切换(尽管它可能为操作系统或其他专用应用程序保留一些核心)。
JobSystem将作业放入作业队列中执行。作业系统中的工作线程从作业队列中获取项目并执行它们。作业系统管理依赖关系
并确保作业以适当的顺序执行。
什么是Job?
Job是完成一项特定任务的一小部分工作。作业接收参数并对数据进行操作,类似于方法调用的行为方式。Job可以是自包含的,也可以依赖其他Job来完成才能运行。
什么是工作依赖( job dependencies)?
在复杂的系统中,例如游戏开发所需的系统,不可能每个工作都是独立的。一项工作通常是为下一项工作准备数据。Jobs
知道并支持依赖项来完成这项工作。如果jobA依赖于jobB,则作业系统确保在完成jobA之前不会开始执行jobB。
4.C#作业系统中的安全系统
Race conditions(竞争条件)
编写多线程代码时,总是存在竞争条件的风险。当一个操作的输出取决于另一个不受其控制的进程的时间时,就会出现竞争条件。
竞争条件并不总是错误,但它是不确定行为的来源。当竞争条件确实导致错误时,可能很难找到问题的根源,因为它取决于时间,因此您只能在极少数情况下重新创建问题。调试它可能会导致问题消失,因为断点和日志记录可以改变单个线程的时间。竞争条件在编写多线程代码时产生了最重大的挑战。
安全系统 为了更轻松地编写多线程代码,Unity C# Job System 检测所有潜在的竞争条件并保护您免受它们可能导致的错误的影响。
例如:如果 C#
作业系统从主线程中的代码向作业发送对数据的引用,则它无法验证主线程是否在作业正在写入数据的同时正在读取数据。这种情况会产生竞争条件。
C# 作业系统通过向每个作业发送它需要操作的数据的副本(也就是NativeContainer)来解决这个问题,而不是对主线程中的数据的引用。此副本隔离了数据,从而消除了竞争条件。
线程发生竞争是个很头疼的问题,这也是为什么JobSystem不让我们去访问主线程。
比如说,主线程有一个变量m,值为5
有两个Job,一个Job控制其m++,一个Job控制其m–,那么这个m在主线程里面数据的准确性就很难保障了,可能被Job随时修改,引发一系列问题
可能执行完成后,Job可能是任何值,可能被第一个Job修改,也可能被第二个Job修改,这显然不是我们想要的结果。数据的安全性是个很重要的问题,这也是我们为什么要少用静态变量的原因。静态变量是任何地方都能访问和修改的,所以很危险。
我们也可能通过加“锁”来控制,但是锁太多,也会提供编程的难度和程序的复杂度
我的GitHub仓库里面有对C#线程的使用教学,以及一些我学习C#做的案例和笔记。
所以,在JobSystem里面,不能传入外部变量,也不能修改外部变量,也不能干扰主线程。只能调用和修改静态变量,或者使用自定义的NativeArray去调用和计算数据,我们在主线程创建NativeArray为其赋值,然后给Job去处理,处理完了之后,在主线程将NativeArray的值拿出来。
下面有对其的详细介绍
5.原生容器(NativeContainer)
安全系统复制数据过程的缺点是它还会隔离每个副本中的作业结果。为了克服这个限制,您需要将结果存储在一种称为NativeContainer的共享内存中。
ANativeContainer是一种托管值类型,可为本机内存提供安全的 C# 包装器。它包含一个指向非托管分配的指针。当与 Unity C# 作业系统一起使用时,aNativeContainer允许作业访问与主线程共享的数据,而不是使用副本。
NativeContainer 的类型 Unity 附带了一个NativeContainer名为NativeArray。您还可以使用NativeSlice 操作 aNativeArray以获取从NativeArray特定位置到特定长度的子集。
注意:实体组件系统(ECS) 包扩展了Unity.Collections命名空间以包括其他类型NativeContainer:
NativeList- 可调整大小的NativeArray. NativeHashMap - 键值对。 NativeMultiHashMap - 每个键有多个值。 NativeQueue- 先进先出 ( FIFO ) 队列。 NativeContainer 和安全系统 安全系统内置于所有NativeContainer类型中。它跟踪任何正在读取和写入的内容NativeContainer。
注意:所有类型的安全检查NativeContainer(例如越界检查、释放检查和竞争条件检查)仅在 Unity编辑器和播放模式中可用。
该安全系统的一部分是DisposeSentinel和AtomicSafetyHandle。DisposeSentinel如果您没有正确释放内存,它会检测内存泄漏并给您一个错误。触发内存泄漏错误发生在泄漏发生很久之后。
使用AtomicSafetyHandle转移NativeContainer代码中的所有权。例如,如果两个计划的作业正在写入同一个NativeArray,安全系统会抛出异常,并带有明确的错误消息,解释为什么以及如何解决问题。当您安排有问题的作业时,安全系统会引发此异常。
在这种情况下,您可以使用 依赖 .第一个作业可以写入NativeContainer,一旦它完成执行,下一个作业就可以安全地读取和写入相同NativeContainer的 . 当从主线程访问数据时,读取和写入限制也适用。安全系统确实允许多个作业并行读取相同的数据。
默认情况下,当作业可以访问 aNativeContainer时,它同时具有读取和写入访问权限。此配置可能会降低性能。C#作业系统不允许您安排一个对 a 具有写访问权限的NativeContainer作业与另一个正在写入它的作业同时进行。
如果作业不需要写入 a NativeContainer,请NativeContainer使用[ReadOnly]属性标记 ,如下所示:
[ReadOnly] public NativeArray<int> input;
在上面的示例中,您可以与其他也对第一个具有只读访问权限的作业同时执行该作业NativeArray。
注意:没有防止从作业中访问静态数据的保护措施。访问静态数据会绕过所有安全系统,并可能导致 Unity 崩溃。有关详细信息,请参阅C#作业系统提示和故障排除。
NativeContainer 分配器 创建 时NativeContainer,您必须指定所需的内存分配类型。分配类型取决于作业运行的时间长度。通过