c# Job system

C# Job System Overview

参考:https://blog.csdn.net/yinfourever/article/details/104378761

How the C# Job System works

The Unity C# Job System能够写多线程 multithreaded code 的代码,这与Unity的其余部分交互良好,并使它更容易编写正确的代码。

编写多线程代码可以提高性能。其中包括帧率的显著提高. 在c#作业中使用Burst编译器(Burst compiler)可以提高代码生成(使用JIT(just in time)即使生成,把自己写的代码编译成辑器语言)质量这也大大减少了移动设备的电池消耗。

c#作业系统的一个重要方面是它集成了Unity内部使用的东西(Unity的本地作业系统). 用户编写的代码and Unity share worker threads. 这种协作避免了创建比CPU内核更多的线程,这将导致对CPU资源的争用。

For more information, watch the talk Unity at GDC - Job System & Entity Component System.

What is multithreading?

在单线程计算系统中,一次输入一条指令,一次输出一个结果. 加载和完成程序的时间取决于需要CPU完成的工作量。

多线程是一种编程类型,它利用CPU在多个核心之间同时处理多个线程. 它们不是一个接一个地执行任务或指令,而是同时运行。

默认情况下,一个线程在程序的开始运行.这是“main thread”. 主线程创建新线程来处理任务.这些新线程之间并行运行, 一旦完成,通常会将它们的结果与主线程同步。

如果您有一些任务需要长时间运行,那么这种多线程方法可以很好地工作.然而,游戏开发代码通常包含许多需要同时执行的小指令.如果您为每个线程创建一个线程,您可能会得到许多线程,每个线程的生命周期都很短. 这可能会使CPU和操作系统的处理能力达到极限。

通过拥有一个线程池,可以减轻线程生存期的问题 pool of threads. 但是,即使使用线程池,也可能有大量线程同时处于活动状态. 拥有比CPU内核更多的线程会导致线程之间争夺CPU资源, 导致频繁的上下文切换. Context switching 是通过执行代码保存一个线程中的状态的过程,然后把这个状态应用于另一个线程,然后会重构第一个线程,稍后,继续处理它. Context switching 是资源密集型的,所以您应该尽可能避免使用它.

What is a job system?

A job system 作业管理系统,通过创建job管理 multithreaded code 而不是创建线程

A job system 通过多个CPU核心管理工作线程manages a group of worker threads across multiple cores. 通常每一个logical CPU core 有一个工作线程, 为了避免 context switching (尽管它可能为操作系统或其他专用应用程序保留一些核心).

A job system 把job添加到 job queue 里面执行. Worker threads in a job system 里面的工作线程从job queue里面获取job,然后执行它们. A job system manages通过 dependencies 来确保job以正确的顺序执行.

What is a job?

A job是一个完成一项特定任务的小工作单元. A job接收参数并对数据进行操作,类似于调用方法. Jobs 可以独立的, 也可以是依赖于其它job,在其它job完成之后,再完成本职工作.

What are job dependencies?

在一个复杂的系统中,比如游戏开发所需要的, 不太可能每个作业都是独立的. One job通常为下一个job准备数据. Jobs 支持这种工作,如果jobA 依赖于 jobB,  job system 确定 jobA 直到jobB完成后才开始执行。

The safety system in the C# Job System:安全的job系统

Race conditions:竞用资源

当使用多线程时 multithreaded code, 总会有资源竞用的风险(多个线程同时操作同一组数据) race conditions. 当一个操作的输出结果依赖于其控制范围之外的另一个进程的时间时,就会出现竞争条件

竞态条件并不总是一个bug,但它是不确定性行为的一个来源. 当竞态条件确实导致bug时,可能很难找到问题的根源,因为它依赖于时间,因此,您只能在极少数情况下重新创建该问题. 调试它可能会导致问题消失,因为断点和日志记录可能会改变各个线程的计时. 竞争条件在编写多线程代码时产生了最大的挑战.

Safety system

以便更容易地编写多线程代码, the Unity C# Job System 检测所有潜在的竞争条件,并保护你免受他们可能造成的错误。

比如:  C# Job System给从主线程中给job发送一个对数据的引用,如果它无法验证主线程是否正在读取数据,同时作业正在向其写入数据,. 这就会造成一个 race condition.

C# Job System通过把它需要操作的数据复制一份,来解决这种问题,而不是直接引用主线程中的引用. 该副本隔离了原始数据,消除了竞争条件

 C# Job System复制数据的方式意味着job只能访问  blittable(托管代码和非托管代码之间是等价的,不用做相应的转换,比如int,在c#和c++里面都是int类型)  类型的数据 data types. 在托管代码和本机代码之间传递时,这些类型不需要转换

 C# Job System 可以通过memcpy方式拷贝 blittable types 类型,并在托管和unity的本机代码之间传送数据. 它使用内存拷贝的方式 memcpy(也就是另辟一块内存存放拷贝的值),然后通过调度job对它操作,然后允许托管端访问该副本 see Scheduling jobs.

NativeContainer

 safety system’s 的缺点就是单独对拷贝数据进行操作的同时,也导致了操作结果的隔离. 为了解决这个问题,那你需要把结果存储在一个名为NativeContainer.共享内存里面。

What is a NativeContainer?

 NativeContainer 是托管值类型,为本机内存提供一个相对安全的c#包装器. 它包含一个指向非托管分配的指针.当使用 Unity C# Job System时,  NativeContainer 允许job 访问与主线程共享的数据,而不是使用副本。

What types of NativeContainer are available?

Unity 分享了一个 NativeContainer 叫做 NativeArray. 你可以使用NativeSlice去操纵一个 NativeArray中获得从特定位置到特定长度的子集

Note: The Entity Component System (ECS) 包扩展了 Unity.Collections 命名空间,让他包含了 NativeContainer的其他类型:

  • NativeList - 一个可调整大小的 NativeArray.
  • NativeHashMap -键和值对.
  • NativeMultiHashMap - 每个键有多个值.
  • NativeQueue -先进先出(FIFO)队列

NativeContainer and the safety system

 safety system安全系统内知道所有的 NativeContainer 类型里面,他跟踪NativeContainer里面的所有读写操作

Note: 所有对NativeContainer 里面类型的安全检测 (比如 越界检查, 回收检查, 和竞态条件检查)只能在Unity编辑器和播放模式。  Unity Editor and Play Mode.

这个安全系统的一部分是 DisposeSentinel and AtomicSafetyHandle. DisposeSentinel检测内存泄漏,并在您没有正确释放内存时给您一个错误. 在内存泄漏之后很长时间才触发内存泄露的错误

AtomicSafetyHandle 在代码中转移一个 NativeContainer 的所有权. 比如:如果你分配两个job同时对同一个NativeArray操作, the safety system 会抛出异常,同时提示你如何解决这个问题.安全系统在您调度有问题的作业时抛出此异常。

在这种情况下,您可以使用依赖项来调度作业. 第一个作业可以写入 NativeContainer,一旦它完成执行,下一个作业就可以安全地读写到相同的 NativeContainer. 当从主线程访问数据时,读写限制也适用. 安全系统允许多个作业并行读取相同的数据。(这个是读取,允许并行读取,不允许并行写入)

默认情况下,当作业可以访问NativeContainer时, 它具有读和写访问权限. 这种配置会降低性能. 在另一个job正在对一个 NativeContainer写入的时候, C# Job System不允许你使用另一个job对其进行写入的访问.

If a job 不需要写入 NativeContainer, 标记 NativeContainer[ReadOnly] attribute, like so:

Creating jobs

在Unity中创建一个job ,需要继承 IJob接口. IJob能够让你并行的执行job,也就是在其它job运行的同时,也能运行你的job.

Note:  “job” 在unity中是继承IJob 接口的结构体的代名词.

要创造一个job,你需要:

当执行 job的时候, Execute 方法会在单个核心上运行一次.

Note: 当你设计你的job的时候,记住它们操作的的对象是复制的数据,除非你把数据放在NativeContainer中,所以,在job中访问主线程数据的唯一方式就是把数据放在 NativeContainer中.

An example of a simple job definition

// Job adding two floating point values together
public struct MyJob : IJob
{
    public float a;
    public float b;
    public NativeArray<float> result;

    public void Execute()
    {
        result[0] = a + b;
    }
}

[ReadOnly]
public NativeArray<int> input;

上面的例子中,可以有多个job同时对NativeArray进行读取访问.

Note: 无法防止从job中访问静态数据.访问静态数据会绕过所有安全系统,破坏Unity. For more information, see C# Job System tips and troubleshooting.

NativeContainer Allocator:内存分配

当创建一个 NativeContainer的时候,你必须声明内存分配的类型. 内存分配的类型取决于job运行的时间长短. 这种方式让你可以根据不同的情况调整内存分配的方式,从而获得最好的性能.

有三种内存分配的类型. 在实例化您的 NativeContainer声明它.

  • Allocator.Temp 有最快的分配. 它用于分配一个比一帧时间或者比一帧时间更少的job. 你不应该将使用temp分配的 NativeContainer传递给 jobs. 你同时需要在一些方法返回之前调用 Dispose方法 (比如 MonoBehaviour.Update, 或者本机代码到托管代码的任何回调方法).
  • Allocator.TempJobTemp慢但比 Persistent快. 它适合分配四帧之间的内存,并且是线程安全的 thread-safe. 如果你在四帧之内没有释放 Dispose 它, 控制台打印警告,大多数小地jobs使用 NativeContainer这个分配类型.
  • Allocator.Persistent 是最慢的分配,但可以根据你的需要一直持续下去. 它是直接调用 malloc. Longer方法的一层封装, . Y在需要性能的地方,不应该使用Persistent

For example:

NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);

Note: 上面例子中的1表示NativeArray的大小. 上面的例子中, 它只有一个数组元素 (因为结果中只存储了一条数据 result).

发布了84 篇原创文章 · 获赞 7 · 访问量 3889

猜你喜欢

转载自blog.csdn.net/qq_37672438/article/details/104668796
今日推荐