C# Job System

概述

设计目的:简单安全地使用多线程,随便就能写出高性能代码

收益:FPS更高,电池消耗更低(Burst编译器)

并行性:C# Job System和Unity Native Job System共享工作线程worker threads,也就是它们不会创建超过CPU cores数量的线程,也就不会导致CPU资源抢占问题。

什么是多线程

单线程:一次执行一条指令,产生一个结果

多线程:利用CPU的多核,多条指令同时执行,其他线程执行完成后会将结果同步给主线程。

多线程好的实践:几个运行时间很长的任务。

游戏代码的特点:大量小而短的任务。

解决方案:线程池

context switching:线程上下文切换,性能敏感的,要尽量避免。

    当激活的线程数超过CPU cores时,就会导致CPU资源争夺,从而触发频繁的context switching。

    过程:先saving执行了一部分的当前线程,然后执行另外的线程,切回来的时候再reconstructing之前的线程再继续执行。

什么是Job System

简化多线程:job system通过创建jobs来实现多线程,而不是直接创建thread。

job概念:完成特定任务的一个小的工作单元。job接收参数并操作数据,类似于函数调用。job之间可以有依赖关系,也就是一个job可以等另一个job完成之后再执行。

job system管理一组worker threads,并且保证一个logical CPU core一个worker thread,避免context switching

job system将jobs放在一个job queue里面,worker threads从job queue里面获取job然后执行。

job依赖性:job system管理job依赖关系,并保证执行时序的正确性

C# Job System的Safety System

Race conditions:竞争条件,一个输出结果依赖于不受控制的事件出现的顺序或时机。

在写多线程代码时,race conditions是一个很大的挑战。race conditions不是bug,但它会导致不确定性行为。并且一旦出现,就很难定位,也很难调试,因为它依赖时机,打断点和加log本身都会改变各个独立线程执行的时机。

Safety system:为了写出更安全的多线程代码,C# Job System会检查所有的潜在的race conditions并保护代码不受可能会产生的bug的影响(这句话有点模糊......)。

解决办法:数据拷贝,每个job操作来自主线程数据的副本,而不是操作原数据。这样数据独立,就不会产生race conditions了。

blittable data types:job只能访问blittable的数据,这些数据在托管代码和native代码之间拷贝的时候,不需要做额外的类型转换。

拷贝方式:memcpy

NativeContainer

NativeContainer实际上是native memory的一个wrapper,包含一个指向非托管内存的指针。

不需要拷贝:使用NativeContainer可以让一个job和main thread共享数据,而不用拷贝。(copy虽然能保证Safety System,但每个job的计算结果也是分开的)。

可使用的C#类型定义:

  

数据结构说明来源
NativeArray数组Unity
NativeSlice可以访问一个NativeArray的某一部分Unity
NativeList一个可变长的NativeArrayECS
NativeHashMapkey value pairsECS
NativeMultiHashMap一个key对应多个valuesECS
NativeQueueFIFO的queueECS

Safety System安全策略:    

  Safety System内置于所有的NativeContainer,会自动跟踪NativeContainer的读写状态。

    注意:所有的safety checkes都只在Editor和PlayMode模式下生效:bounds checks、deallocation checks、race condition checks。

    还有一部分安全策略:

        DisposeSentinel:自动检测memory leak并报错。依赖宏定义ENABLE_UNITY_COLLECTIONS_CHECKS。

        AtomicSafetyHandle:用来转移NativeContainer的控制权。比如当2个jobs同时写一个NativeContainer,Safety System就会抛出一个error,并描述如何解决。异常会在产生冲突的job调度时抛出。依赖宏定义ENABLE_UNITY_COLLECTIONS_CHECKS。

        这种情况下,可以使用job依赖,让其中一个job依赖另外一个job的完成。

规则:Safety System允许多个job同时read同一块数据。

规则:Safety System不允许一个job正在writing数据时,调度激活另一个“拥有write权限”的job(不是不让同时write)。

规则:手动指定job对数据的只读:(默认是可读写,会影响性能)

    [ReadOnly]public NativeArray<int> input;

  注意:job对static data的访问没有Safety System安全保护,所以使用不当可能造成crash。

 

NativeContainer Allocator分配器:

(1)Allocator.Temp

    最快,维持1 frame,job不能用,需要手动Dispose(),比如可以再native层的callback调用时使用。

(2)Allocator.TempJbo

    稍微慢一点,最多维持4 frames,thread-safe,如果4 frames内没有Dispose(),会有warning。大多数small jobs都会使用这个类型的分配器.

(3)Allocator.Persistent

    最慢,但是可持久存在,就是malloc的wrapper。Longer jobs使用这个类型,但在性能敏感的地方不应该使用。

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

创建Job

三要素:

(1)创建一个struct实现接口IJob;

(2)添加数据成员:要么是blittable类型, 要么是NativeContainer;

(3)添加Execute()方法实现。

执行job时,job.Execute()方法会在一个cpu core上执行一次。

注意:job操作数据是基于拷贝的,除非是NativeContainer类型。那么,一个job访问main thread数据的唯一方式就是使用NativeContainer。

复制代码

public struct TestJob : IJob
{public float a;public float b;public NativeArray<float> result;public void Execute(){result[0] = a + b;}
}

复制代码

调度Job

三要素:

(1)实例化job;

(2)设置数据;

(3)调用job.Schedule()方法。

调用Schedule方法会将job放到job queue里面等待执行。一旦开始schedule,就没法中断job了。(疑问:这个once scheduled,是job.Schedule方法,还是从job queue里面拿出来开始执行?)

复制代码

private void TestScheduleJob()
{// Create a native array of a single float to store the result. This example waits  for the job to complete for illustration purposesNativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);// Set up the job dataMyJob jobData = new MyJob();jobData.a = 10;jobData.b = 10;jobData.result = result;// Schedule the jobJobHandle handle = jobData.Schedule();// Wait for the job to completehandle.Complete();// All copies of the NativeArray point to the same memory, you can access the  result in "your" copy of the NativeArrayfloat aPlusB = result[0];// Free the memory allocated by the result arrayresult.Dispose();
}

复制代码

JobHandle和Job依赖

设置job依赖关系:

JobHandle firstJobHandle = firstJob.Schedule();
secondJob.Schedule(firstJobHandle);

secondJob依赖firstJob的结果。

组合依赖项:

NativeArray<JobHandle> handles = new NativeArray<JobHandle>(numJobs, Allocator.TempJob);
// Populate `handles` with `JobHandles` from multiple scheduled jobs...
JobHandle jh = JobHandle.CombineDependencies(handles);

在main thread中等待jobs执行完成:

    flush job:使用JobHandle.Complete()来等待job执行完成。

    job只有Schedule之后才会执行,如果你想在main thread中访问job的正在使用的数据,你可以调用JohHandle.Comlete()。该方法flush job,并开始执行,然后将NativeContainer的数据权限返回给main thread。

    如果你不需要访问数据,也可以调用统一static flush函数:JobHandle.ScheduleBatchedJobs(),当然该方法会影响到性能。

复制代码

public struct MyJob : IJob
{public float a;public float b;public NativeArray<float> result;public void Execute(){result[0] = a + b;}
}
public struct AddOneJob : IJob
{public NativeArray<float> result;public void Execute(){result[0] = result[0] + 1;}
}private void TestScheduleJob()
{NativeArray<float> result = new NativeArray<float>(1, Allocator.TempJob);MyJob jobData = new MyJob();jobData.a = 10;jobData.b = 10;jobData.result = result;JobHandle firstHandle = jobData.Schedule();AddOneJob incJobData = new AddOneJob();incJobData.result = result;JobHandle secondHandle = incJobData.Schedule(firstHandle);secondHandle.Complete();float aPlusB = result[0];result.Dispose();
}

复制代码

ParallelFor jobs 并行job

IJob只能一次一个job执行一个任务,但游戏开发中经常需要重复执行某个动作很多次,这时候就可以用到并行任务IJobParallelFor。

    ParallelFor jobs使用NativeArray作为数据源,并且运行在多个core上,还是一个job一个core,只是每个job只负责处理完整数据的一个子集。

    Execute(idx)方法对于数据源NativeArray中的每个item都调用一次。

 调度:

  需要手动指定执行次数,表示需要分多少次独立Execute来执行,一般直接取NativeArray的数组长度作为执行次数,一次处理一个数据。       

  

当一个native job提前完成它的batches,它会从其他的native job偷取一部分batches,然后继续执行。

颗粒度问题:分得太细会有work不断重建的开销,分得太粗又会有单核负载问题。

尝试法:所以最佳实践是从1开始逐步增加,直到性能不再提高。

复制代码

public struct MyParallelJob : IJobParallelFor
{public NativeArray<float> a;public NativeArray<float> b;public NativeArray<float> result;public void Execute(int index){result[index] = a[index] + b[index];}
}private void TestScheduleParallelJob()
{NativeArray<float> a = new NativeArray<float>(10, Allocator.TempJob);NativeArray<float> b = new NativeArray<float>(10, Allocator.TempJob);NativeArray<float> result = new NativeArray<float>(10, Allocator.TempJob);for(int i = 0; i < 10; ++i){a[i] = i * 0.3f;b[i] = i * 0.5f;}MyParallelJob jobData = new MyParallelJob();jobData.a = a;jobData.b = b;jobData.result = result;JobHandle handle = jobData.Schedule(10, 1);handle.Complete();for(int i = 0; i < 10; ++i){Debug.LogError(result[i]);}a.Dispose();b.Dispose();result.Dispose();
}

复制代码

ParallelForTransform jobs

public struct MyTransformParallelJob : IJobParallelForTransform
{public void Execute(int index, TransformAccess transform){}
}

注意事项:

(1)不能在job中访问static数据

    在job中访问static数据是没有Safety System保证的,可能会导致crash。unity后续版本会增加static analysis来阻止这种用法。

 

(2)Flush scheduled batchs

    JobHandle.ScheduleBatchedJobs:当你想要你的job开始执行是,可以调用这个函数flush调度的batch。

    不flush batch会导致调度延迟到主线程等待batch执行结果时才触发执行。

    JobHandle.Complete:直接开始执行。

    在ECS中,batch flush是隐式执行的,不需要手动调用JobHandle.ScheduleBatchJobs。

    

(3)不要试图更新NativeContainer的内容

    因为缺乏ref returns机制,所以不要这样用:

复制代码

    nativeArray[0]++;// 等同于:var tmp = nativeArray[0];tmp++;// 不生效!// 正确的写法是:var tmp = nativeArray[0];tmp++;nativeArray[0] = tmp;MyStruct temp = myNativeArray[i]; temp.memberVariable = 0;myNativeArray[i] = temp;

复制代码

(4)调用JobHandle.Complete来让main thread重获控制权

    主线程在访问数据之前,需要依赖的job调用complete。不能只是check JobHandle.IsCompleted,而是需要手动调用JobHandle.Complete()。

    此调用还会清理Safety System的状态,不调用的话会有内存泄漏。

 

(5)在主线程中使用Schedule和Complete

    这两个函数只能在主线程中调用。不能因为一个job依赖另一个job,就在前一个job中手动schedule另一个job。

 

(6)在正确的时间使用Schedule和Complete

    Schedule:在数据填充完毕,立马调用

    Complete:只在你需要result的时候调用

    

(7)NativeContainer添加read-only标记

    默认是可读写的,如果确定只读就标记为read-only,可以提升性能。

 

(8)检查数据依赖

    如果在profiler里看到main thread有“WaitForJobGroup”,就表示在等待worker thread处理完成。也就是说你的代码里面在什么地方引入了一个data dependency,这时候可以通过检查JobHandle.Complete来看一下是什么依赖关系导致了main thread需要等待的情况。

 

(9)调试jobs

    Jobs有一个Run函数,你可以用它来替换原本调用Schedule的地方,从而在main thread上立即执行这个job。可以使用这个方法来调试。

 

(10)不要在job里面分配托管内存managed memory

    在job里面分配托管内存是非常慢的,而且会导致Burst compiler没法使用。

    Burst是基于LLVM的后端编译技术,它可以利用平台特定能力将c# jobs代码编译成高度优化过的机器码。

 

Unity GDC 2018: C# Job System

https://www.youtube.com/playlist?list=PLX2vGYjWbI0RuXtGMYKqChoZC2b-H4tck

 

Unity at GDC - Job System & Entity Component System

https://www.youtube.com/watch?v=kwnb9Clh2Is&t=1s

 

Job System介绍

http://www.pianshen.com/article/634466006/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/246225.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity游戏开发——C#特性Attribute与自动化

这篇文章主要讲一下C#里面Attribute的使用方法及其可能的应用场景。 比如你把玩家的血量、攻击、防御等属性写到枚举里面。然后界面可能有很多地方要根据这个枚举获取属性的描述文本。 比如你做网络框架的时候&#xff0c;一个协议号对应一个类的处理或者一个方法。 比如你做…

Unity c#中Attribute用法详解

举两个例子&#xff0c;在变量上使用[SerializeFiled]属性&#xff0c;可以强制让变量进行序列化&#xff0c;可以在Unity的Editor上进行赋值。 在Class上使用[RequireComponent]属性&#xff0c;就会在Class的GameObject上自动追加所需的Component。 以下是Unity官网文档中找…

走进LWRP(Universal RP)的世界

走进LWRP&#xff08;Universal RP&#xff09;的世界 原文&#xff1a;https://connect.unity.com/p/zou-jin-lwrp-universal-rp-de-shi-jie LWRP自Unity2018发布以来&#xff0c;进入大家视野已经有一段时间了&#xff0c;不过对于广大Unity开发者来说&#xff0c;依然相对…

Unity 2017 Game Optimization 读书笔记(1)Scripting Strategies Part 1

1.Obtain Components using the fastest method Unity有多种Getcomponet的方法&#xff1a; GetComponent(string), GetComponent<T>() GetComponent(typeof(T)) 哪种效率最高会跟随Unity版本的变化而变化&#xff0c;对于Unity 2017&#xff0c;本书作者的测试是Ge…

C# 多态相关的文章

一 C# 多态的实现 封装、继承、多态&#xff0c;面向对象的三大特性&#xff0c;前两项理解相对容易&#xff0c;但要理解多态&#xff0c;特别是深入的了解&#xff0c;对于初学者而言可能就会有一定困难了。我一直认为学习OO的最好方法就是结合实践&#xff0c;封装、继承在…

C++ 虚函数和虚表

几篇写的不错的文章&#xff0c;本文是整合了这几篇文章&#xff0c;感谢这些大佬 https://www.jianshu.com/p/00dc0d939119 https://www.cnblogs.com/hushpa/p/5707475.html https://www.jianshu.com/p/91227e99dfd7 多态: 多态是面相对象语言一个重要的特性,多态即让同一…

Unity 2017 Game Optimization 读书笔记(2)Scripting Strategies Part 2

1. Share calculation output 和上一个Tip很像&#xff0c;可以缓存计算结果或者各种信息&#xff0c;避免多次重复的计算&#xff0c;例如在场景里查找一个物体&#xff0c;从文件读取数据&#xff0c;解析Json等等。 容易忽略的点是常常在基类了实现了某个方法&#xff0c;在…

Unity 2017 Game Optimization 读书笔记(3)Scripting Strategies Part 3

1.Avoid retrieving string properties from GameObjects 通常来讲&#xff0c;从C#的object中获取string 属性没有额外的内存开销&#xff0c;但是从Unity中的Gameobject获取string属性不一样&#xff0c;这会产生上一篇讲到的 Native-Managed Bridge&#xff08;Native内存和…

Unity 2017 Game Optimization 读书笔记(4)Scripting Strategies Part 4

1.Avoid Find() and SendMessage() at runtime SendMessage() 方法和 GameObject.Find() 相关的一系列方法都是开销非常大的。SendMessage()函数调用的耗时大约是一个普通函数调用的2000倍&#xff0c;GameObject.Find() 则和场景的复杂度相关&#xff0c;场景越复杂&#xff0…

Unity HDRP中的光照烘焙测试(Mixed Lighing )和间接光

部分内容摘抄自&#xff1a;https://www.cnblogs.com/murongxiaopifu/p/8553367.html 直接光和间接光 大家都知道在Unity中&#xff0c;我们可以在场景中布置方向光、点光、聚光等类型的光源。但如果只有这些光&#xff0c;则场景内只会受到直接光的影响&#xff0c;而所谓的…

聊聊Unity项目管理的那些事:Git-flow和Unity

感谢原作者https://www.cnblogs.com/murongxiaopifu/p/6086849.html 0x00 前言 目前所在的团队实行敏捷开发已经有了一段时间了。敏捷开发中重要的一个话题便是如何对项目进行恰当的版本管理。项目从最初使用svn到之后的Git One Track策略再到现在的GitFlow策略&#xff0c;中…

聊聊网络游戏同步那点事

写的非常好的一篇博文&#xff0c;转载自https://www.cnblogs.com/murongxiaopifu/p/6376234.html 0x00 前言 16年年底的时候我从当时的公司离职&#xff0c;来到了目前任职的一家更专注于游戏开发的公司。接手的是一个platform游戏项目&#xff0c;基本情况是之前的团队完成…

Unity 2017 Game Optimization 读书笔记 Dynamic Graphics(1)

The Rendering Pipeline 渲染表现差有可能取决于CPU端&#xff08;CPU Bound&#xff09;也有可能取决于GPU(GPU Bound).调查CPU-bound的问题相对简单&#xff0c;因为CPU端的工作就是从硬盘或者内存中加载数据并且调用图形APU指令。想找到GPU-bound的原因会困难很多&#xff…

Unity 2017 Game Optimization 读书笔记 Dynamic Graphics(2)

Lighting and Shadowing 现代的游戏中&#xff0c;基本没有物体能在一步就完成渲染&#xff0c;这是因为有光照和阴影的关系。光照和阴影的渲染在Fragment Shader中需要额外的pass。 首先要设置场景中的Shadow Casters和Shadow Receivers&#xff0c;Shadow Casters投射阴影&…

Unity 2017 Game Optimization 读书笔记 The Benefits of Batching

batching&#xff08;合批&#xff09; 和大量的描述一个3D物体的数据有关系&#xff0c;比如meshes&#xff0c;verices&#xff0c;edges&#xff0c;UV coordinates 以及其他不同类型的数据。在Unity中谈论batching&#xff0c;指的是用于合批mesh数据的两个东西&#xff1a…

Unity 2017 Game Optimization 读书笔记 Dynamic Graphics (3)

Rendering performance enhancements Enable/Disable GPU Skinning 开启GPU Skinning可以减轻CPU或GPU中Front End部分中某一个的负担&#xff0c;但是会加重另一个的负担。Skinning是mesh中的顶点根据动画中骨骼的当前位置进行计算&#xff0c;从而让角色摆出正确的姿势。 …

Unity手游开发札记——布料系统原理浅析和在Unity手游中的应用

原文&#xff1a;https://zhuanlan.zhihu.com/p/28644618 0. 前言 项目技术测试结束之后&#xff0c;各种美术效果提升的需求逐渐成为后续开发的重点&#xff0c;角色效果部分的提升目标之一便是在角色选择/展示界面为玩家提供更高的品质感&#xff0c;于是可以提供动态效果的…

行为树(Behavior Tree)实践(1)– 基本概念

原文&#xff1a;http://www.aisharing.com/archives/90 行为树&#xff08;Behavior Tree&#xff09;实践&#xff08;1&#xff09;– 基本概念 自从开博以来&#xff0c;每天都会关心一下博客的访问情况&#xff0c;看到一些朋友的订阅或者访问&#xff0c;不胜欣喜&…

Unity 2017 Game Optimization 读书笔记 Dynamic Graphics (5) Shader优化

Shader optimization Fill Rate和 Memory Bandwidth开销最大的地方就是Fragment Shader。开销多大取决于Fragment Shader的复杂程度&#xff1a;多少纹理需要采样&#xff0c;多少数学计算函数需要使用等等。GPU的并行特性意味着在线程中如果任何地方存在瓶颈&#xff0c;都会…

Unity 2017 Game Optimization 读书笔记 Dynamic Graphics (6)

1. Use less texture data 这条优化技巧非常直接&#xff0c;减少texture的数据量&#xff0c;减少分辨率或者降低位数&#xff0c;虽然可能会降低渲染质量。但是通常使用16-bit textures并不会明显的感觉到渲染效果下降。 MipMap技术可以有效减少VRAM和Texture Cache之间来回…