https://mp.weixin.qq.com/s/IY_zmySNrit5H8i0CcTR7Q

通常而言,最好不要把Unity实体组件系统ECS和Job System看作互相独立的部分,要把它们看作用于大幅提升游戏性能的组合系统。

本系列文章我们将深入了解使用二者开发项目的过程,从而使项目获得高性能。今天我们来了解ECS和Job System的基础知识,了解ECS请阅读:《详解实体组件系统ECS》。

什么是Job System

一些人认为Unity无法进行多线程处理,那个观点是错的,因为这是可以实现的,但是你可能无法使用任何Unity中特定的命名空间。你可以多线程处理不同类型的任务,只要任务不需要在主线程外访问Transform或游戏对象即可,所以在独立线程执行一些Vector3数学运算是没有问题的。

如果你非常了解Unity相关知识,或许你已经知道引擎的部分功能已经实现多线程处理。现在加入Job System后,Unity允许我们利用它的多线程处理功能。

Job System允许我们轻松编写多线程代码,从而实现高性能游戏体验。它不仅能改善帧率,而且在做移动开发时,它还能显著改善移动设备的电池寿命。

通过该功能,我们能够编写和Unity引擎功能共享工作线程的代码。

什么是多线程处理

通常在单线程程序中,每次只处理一个执行调用,一次只输出一个结果。

程序性能主要取决于加载和完成所用的时间。单线程会按线性顺序进行处理,需要的时间会比双线程同时处理更长,这种多个线程同时处理就是我们说的多线程处理。

多线程处理会利用CPU功能来同时在多个内核处理多个线程。

默认情况下,“主线程”会在程序开始时运行。主线程会创建新线程来处理任务。这些新线程会并行运行,通常在完成后将结果与主线程同步。

多线程处理方法适合用来处理多个需要长时间运行的任务。然而,游戏开发代码通常带有很多需要同时执行的小指令。如果为每个小指令都创建一个线程,结果会得到很多线程,每个线程的生命周期都很短。从而导致CPU和操作系统处理能力达到极限。

你可以通过线程池来解决线程生命周期的问题,然而即使使用线程池,还是会同时有很多活动线程。如果线程数量比CPU内核数量多,会造成线程互相竞争CPU资源,并且频繁切换上下文(Context switching)。

上下文切换是指切换线程时,会保存当前进程的执行状态,然后处理另一线程,在重构第一个线程后,继续处理该线程。上下文切换是个资源密集型过程,所以要尽量避免该过程。

Job System和传统多线程的区别

在多线程处理时,要打开线程然后提供任务。你需要注意将辅助线程合并到主线程的时间,还要正确关闭线程。所以多线程处理需要你管理很多操作。

Job System使用不同的方法,因为我们不会创建任何线程,而是会使用Unity在多个内核上的工作线程,给它们提供任务-Unity称之为Jobs作业。很容易看出,这种方法更为简单,因为避免了管理线程时可能遇到的问题。不仅如此,我们还不必担心出现竞态条件。

通过内置的安全检查,Job System可以检测所有潜在的竞态条件。通过给每个作业发送需要处理的数据副本而不是在主线程引用数据,Job System可以避免发生竞态条件,进而消除竞态条件,因为现在处理的是独立数据而不是它的引用。

因此,作业只能访问blittable数据类型。当在托管代码和本地代码之间传递数据时,该类型数据不需要转换。

Unity使用C++方法复制的内存块在Unity的托管部分和本地部分复制和传递数据。在调度作业时,我们会将数据放入本地内存,并在执行作业的同时允许托管部分访问数据副本。

你甚至不必担心发生上下文切换和CPU争用,因为Unity通常在每个CPU内核有一个工作线程,作业会在这些线程间同步调度。

Job System中,所有作业都会放入队列中。空闲工作线程会获取作业,并按照队列的顺序执行。为了确保作业按照所需顺序执行,我们可以利用作业依赖。

Job是什么

总的来说,每个作业(Job)都可以看作是方法调用,每个作业在创建时会得到数据和参数,之后用于执行过程。作业可以是独立的,这意味着当它们什么时候完成对我们来说并不重要。或者在更合理情况下,它们可以拥有依赖。依赖能为我们带来便利,因为它能让代码在正确的时间执行。

对多线程处理来说,这非常重要,你需要确保执行过程能避免发生竞态条件,这意味着一项任务不必等待其它任务完成才执行,那样会造成延迟。

所以基本上,依赖意味着我们的第二个任务依赖于第一个任务,第二个任务会在第一个任务完成后才开始执行。

句法

每个作业都需要实现以下三个类型的其中一个类型:IJob、IJobParallelFor或 IJobParallelForTransform。

IJobParallelFor用于需要多次并行执行单个任务的作业。JobParallelForTransform和IJobParallelFor差不多,尤其是用于处理Unity Transform时。

这些类型实际上都是接口,因此只要脚本中没有Execute函数,编译器就会出问题。还要记住,作业必须是nullable类型,这意味着它必须是struct,并且在任何情况下都不能是类,这是因为内存分配问题。

Unity创建新容器是为了让我们能够很容易就写出线程安全的代码。

using Unity.Collections;
using Unity.Jobs;

/*作业(Job)需要是可空类型,这意味着它们必须为struct结构…

每个作业都必须继承自IJobParallelFor、IJobParallelForTransform或IJob*/

Every job has to inherit from either IJobParallelFor, IJobParallelForTransform or IJob */
public struct MyJob : IJobParallelFor {

/*在作业中,需要定义所有用于执行作业和输出结果的数据

Unity会创建内置数组,它们大体上和普通数组差不多,但是需要自己处理分配和释放设置*/

public NativeArray<Vector3> waypoints;
 public float offsetToAdd;

/*所有作业都需要Execute函数*/

public void Execute(int i)
 {

/*该函数会保存行为。要执行的变量必须在该struct开头定义。*/

waypoints[i] = waypoints[i] * offsetToAdd;
 }
}

调度作业

现在已创建MyJob.cs struct,要如何使它工作呢?我们必须调度它。

通常该过程非常简单,但需要注意,每个作业都需要被调度。那意味着我们首先发起作业,添加数据,然后发送到队列中等待执行。一旦该过程发生,我们就无法中断该过程。

Unity提供的常见句法参考中的作业代码如下:

// 创建单个浮点数的本地数组(NativeArray)来存储结果。为了更好说明功能,该示例会等待作业完成。

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();

这些正确的代码,它可以正常执行,但带有一些缺点,因为在调度完成后进行完成调用会产生短暂的等待时间,在性能分析器中,该时间称为“Idle Time”。

相反如果你习惯调度作业,性能分析器中显示的等待时间将最小化,而且会得到不错的性能,至少在旧机器上效果会很明显。

高效调度作业

在调度作业后,因为工作线程没有时间完成任何任务。这造成在调度调用期间会产生空闲时间,会对性能产生影响。

本示例中,我们会创建struct,保存对句柄和本地数组的引用。为什么保存这些内容?

保存句柄是为了在之后调用作业,保存本地数组是因为需要释放本地数组,NativeArray和常规数组的工作方式差不多,但是需要设置Allocator,用来定义数组在内存中的保留时间,本示例中使用Allocator.TempJob。

我们还需要在调用完成时释放内存,然后复制数据。我们创建了JobResultAndHandle的引用,然后对它调用ScheduleJob()。这会使我们的作业开始调度,而且它的引用会保存在列表中。

然后我们可以查看列表中的每个条目,调用完成,复制执行数据,然后弃用NativeArray来释放内存。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Collections;
using Unity.Jobs;

public class MyJobScheduler : MonoBehaviour 
{
 Vector3[] waypoints;
 float offsetForWaypoints;

//我们将保存结果和句柄的列表

List<JobResultAndHandle> resultsAndHandles = new List<JobResultAndHandle>();

void Update() 
 {

/*我们会在需要时创建新的JobResultANdHandle(该代码不必在Update方法中,因为它只是个示例)

然后我们会给ScheduleJob方法提供引用。*/

JobResultAndHandle newResultAndHandle = new JobResultAndHandle();
   ScheduleJob(ref newResultAndHandle);

/*如果ResultAndHAndles的列表非空,我们会在该列表进行循环,了解是否有需要调用的作业。*/

if(resultsAndHandles.Count > 0)
   {
     for(int i = 0; i < resultsAndHandles.Count; i++){
       CompleteJob(resultsAndHandles[i]);
     }
   }
 }

/* ScheduleJob会获取JobResultAndHandle的引用,初始化并调度作业。

void ScheduleJob(ref JobResultAndHandle resultAndHandle)
 {

//我们会填充内置数组,设置合适的分配器

resultAndHandle.waypoints = new NativeArray<Vector3>(waypoints, Allocator.TempJob);

//我们会初始化作业,提供需要的数据

MyJob newJob = new MyJob
   {
     waypoints = resultAndHandle.waypoints,
     offsetToAdd = offsetForWaypoints,
   };

//设置作业句柄并调度作业

resultAndHandle.handle = newJob.Schedule();
   resultsAndHandles.Add(resultAndHandle);
 }

//完成后,我们会复制作业中处理的数据,然后弃用弃用内置数组

//这一步很有必要,因为我们需要释放内存

void CompleteJob(JobResultAndHandle resultAndHandle)
 {
   resultsAndHandles.Remove(resultAndHandle);

resultAndHandle.handle.Complete();
   resultAndHandle.waypoints.CopyTo(waypoints);
   resultAndHandle.waypoints.Dispose();
 }
}

struct JobResultAndHandle
{
 public NativeArray<Vector3> waypoints;
 public JobHandle handle;
}

JobHandles和依赖

对作业调用Schedule()会使它返回JobHandle。JobHandle对保留作业的引用非常有用,但也可以将它们用作其它作业的依赖。这是什么意思呢?

如果某个作业依赖其它作业的结果,我们可以将其它作业的句柄作为参数传递到myjobs调度方法中,这样能让该作业完成后执行我们的作业。

前文中提到的竞态条件问题、线程等待线程的问题,以及使用多线程代码的缺点问题都可以通过传递句柄来轻松避免。

小结

本文我们了解了Job System的基础知识,在下一篇中我们将以网格变形项目为示例,讲解Job System的使用,尽请期待!更多Unity最新功能介绍尽在Unity官方中文论坛(UnityChina.cn)!

本文来源:http://www.itskristin.me/

深入解读Job System(1)的更多相关文章

  1. 深入解读Job system(2)

    https://mp.weixin.qq.com/s/vV4kqorvMtddjrrjmOxQKg 上一篇文章中,我们讲解了Job System的基础知识,本文将以网格变形项目为示例,讲解Job Sy ...

  2. System.nanoTime与System.currentTimeMillis的理解与区别

    System类代表系统,系统级的很多属性和控制方法都放置在该类的内部.该类位于java.lang包. 平时产生随机数时我们经常拿时间做种子,比如用System.currentTimeMillis的结果 ...

  3. System.currentTimeMillis()计算方式与时间的单位转换

    目录[-] 一.时间的单位转换 二.System.currentTimeMillis()计算方式 一.时间的单位转换 1秒=1000毫秒(ms) 1毫秒=1/1,000秒(s)1秒=1,000,000 ...

  4. 关于System.currentTimeMillis()

    一.时间的单位转换 1秒=1000毫秒(ms) 1毫秒=1/1,000秒(s)1秒=1,000,000 微秒(μs) 1微秒=1/1,000,000秒(s)1秒=1,000,000,000 纳秒(ns ...

  5. Java中系统时间的获取_currentTimeMillis()函数应用解读

    快速解读 System.currentTimeMillis()+time*1000) 的含义 一.时间的单位转换 1秒=1000毫秒(ms) 1毫秒=1/1,000秒(s)1秒=1,000,000 微 ...

  6. I.MX6 Android 5.1 快速合成系统

    /**************************************************************************** * I.MX6 Android 5.1 快速 ...

  7. java多态/重载方法——一个疑难代码引发的讨论

    直接上代码,看这个代码发现自己的基础有多差了.参考 http://www.cnblogs.com/lyp3314/archive/2013/01/26/2877205.html和http://hxra ...

  8. 高通 添加 cmdline

    最近需要设置一个只读的属性值,采用的方法是在cmdline中添加,然后在init进程中解读. 记录一下代码跟踪过程. lk/app/aboot/aboot.c static const char *u ...

  9. 韩顺平Java(持续更新中)

    原创上课笔记,转载请注明出处 第一章 面向对象编程(中级部分) PDF为主 1.1 IDEA 删除当前行,ctrl+y 复制当前行,ctrl+d 补全代码,alt+/ 添加或者取消注释,ctrl+/ ...

随机推荐

  1. C语言 字符串中数字的运算

    主函数中输入字符串"32486"和"12345",在主函数中输出的函数值为44831. #include <stdio.h> #include &l ...

  2. java流的操作步骤、、

    在java中使用IO操作必须按照以下的步骤完成: 使用File找到一个文件 使用字节流或字符流的子类为OutputStream.InputStream.Writer.Reader进行实例化操    作 ...

  3. Mockito为什么不能mock静态方法

    因为Mockito使用继承的方式实现mock的,用CGLIB生成mock对象代替真实的对象进行执行,为了mock实例的方法,你可以在subclass中覆盖它,而static方法是不能被子类覆盖的,所以 ...

  4. python中的 ' ' 和 " "

    #!/usr/bin/python import MySQLdb try: conn = MySQLdb.connect(host = 'localhost', user = 'root', pass ...

  5. JavaScript基本概念C - 真与假

    真与假 与 c 和 c++ 非常相似, 但与 Java 不同, JS中被认为true或false范围很广.所有对象 (空字符串除外) 和非零数字都被视为 true.空字符串.零.null 和undef ...

  6. 第十六章 Velocity工作原理解析(待续)

    Velocity总体架构 JJTree渲染过程解析 事件处理机制 常用优化技巧 与JSP比较 设计模式解析之合成模式 设计模式解析之解释器模式

  7. java 多线程系列基础篇(九)之interrupt()和线程终止方式

    1. interrupt()说明 在介绍终止线程的方式之前,有必要先对interrupt()进行了解.关于interrupt(),java的djk文档描述如下:http://docs.oracle.c ...

  8. 使用mui框架后a标签无法跳转

    由于最近工作项目上使用到前台mui框架,笔者在将H5转换为jsp时,遇见各种各样问题,原因归结为对mui框架不熟悉,今天就遇见一个特别奇怪的问题,界面中超链接<a>标签无法跳转,笔者试着添 ...

  9. Linux 下安装redis

    记录一下linux下的安装步骤,还是比较复杂的 1. 下载redis-2.8.19.tar.gz: ftp传到linux01上: 解压: tar –zxvf redis-2.8.19.tar.gz 2 ...

  10. 电子模块 001 --- 遥杆 JoyStick

    电子模块 001 - 遥杆 JoyStick - Ongoing - 2016年8月31日 星期三 遥杆 JoyStick 模块 今天介绍:JoyStick 电子模块. 模块名称: 双轴按键摇杆 PS ...