【Unity】Unity中的异步编程技术详解

异步编程技术对于不少手游开发者来讲,都是不可避免的话题,由于手游的游戏逻辑包含太多须要并发或者但愿可以并行的逻辑。如今的手机硬件发展迅速,多核已成为主导趋势,对于3A级大做来讲,如何充分利用手机多核的性能从而解放主线程压力就显得尤为重要。本文将由Unity技术支持工程师刘伟贤,为你们分享Unity中的一些异步编程技术,来释放硬件的潜能。web

【关于多线程】

其实众多的Unity游戏性能负荷集中在主线程。从Unity 5.4开始已将大量的Camera.Render工做从主线程当中移除,但这还远远不够。算法

咱们为PS 4平台添加了其它Worker Thread对原生渲染的支持,这意味着渲染请求能够直接调用底层原生的渲染API,而不须要再与Unity的渲染线程进行交互。这能够大幅提升游戏的渲染效率,某些状况下游戏的实际帧率能够提升到原来的2.5倍之多。编程

固然,Unity还会持续添加对其余平台原生渲染API的支持。安全

在最新的Unity 5.6中,咱们加入了对Vulkan的支持。目前Vulkan仅支持Windows Standalone、Linux Standalone和Android平台,而Unity编辑器暂时还不支持Vulkan。数据结构

【关于Unity Job System(Worker Thread)】

Unity会致力于把一些计算量比较重的系统挪到其余的工做线程上去,例如粒子系统、动画系统、布料计算、遮挡剔除、视锥体剔除、蒙皮和静态裁剪等等。Unity Job System就是为了更好地让这些底层系统可以在多线程下安全高效的并行运做。也就是咱们常常在Profiler底下看到的多个Worker Threads。多线程

下面来看一个具体的例子。并发

能够看到在Particle System的运行过程当中,有一个渲染线程叫PutGeometryJobFence,它用于安排各个工做线程进行各个粒子的GeometryJob,等全部的GeometryJob完成之后就会进行渲染。异步

从上面的例子能够看出,Unity内部的Job System是基于任务式的高效安全的多线程系统,很是高效且安全,它基于无锁栈与无锁队列构建这个系统,经过汇编来编写,针对不一样的CPU有不一样的实现。因此,高度优化的Jobs之间存在必定的依赖关系,而且会根据主线程的须要去调整它们的优先级。编辑器

下面来看一个例子。当咱们为Woker Thread安排了一个动画的Job,但若是这时经过脚本去删除Animator和Transform组件,就须要立刻完成全部的Jobs而且将Animator/Transform数据的处理完成。svg

关于异步操做

Unity还提供了一系列的异步操做,方便你们根据须要进行选择。这些异步操做包括Resources的加载、Asset Bundle的加载、场景的加载,NavMesh的生成等等。固然,这些异步操做的底层实现各不相同。有些是独立线程处理的,有些则是利用Unity内部的JobSystem,也有一些是两者结合使用的。

上图的示例中,调用AssetBundle.LoadFromFileAsync时立刻建立一个AsyncOperation,而后立刻交由Job System去处理文件的IO相关操做,等Job System处理完以后就交由PreloadManager去处理Asset Bundle的加载相关操做,而PreloadManager就是叫“UnityPreload”的独立加载线程,并且它是基于队列式的加载管理。

【协程】

协程并不是多线程,它仍是在主线程上执行的,但协程能够将一个函数分红多个部分来顺序执行,从而实现等同并发的处理方式。


在使用协程时要特别注意它们的调用时机。 C#编译器会帮咱们建立一个协程的类,而在开启一个协程时就会建立对应的对象,这个对象用来维护屡次调用时协程的状态。正是要维护这些状态,因此协程内的本地变量也须要放到堆上,启动一个Coroutine所引发的内存消耗等同于一个类的固定成本加上这个 Coroutine所用到的局部变量总内存。而协程的生命周期就是跟着MonoBehaviour来走的。

须要注意的是:
为了减小主线程的CPU开销,须要避免在协程内进行一些阻塞的操做;
在协程内分配的资源要在协程结束之后才会释放,因此不要在协程内循环分配资源;
尽量使用最少的协程数去完成最多的操做;
使用巢状式的协程有助于保持代码简洁且易于维护,但它们也比较容易致使较高的内存开销。

【线程】

首先区别于协程,线程是并行的,是利用了硬件的多核特性。而协程是并发的,它其实仍是在主线程执行。C#的线程是基于.Net的实现,Mono进一步细分操做系统的进程到一个轻量级的托管子进程,这就是AppDomain。而在AppDomain中又能够存在一个或者多个的托管线程,也就是Sytem.Threading.Thread。

  • 托管线程并不须要映射到单独的原生线程上;
  • 原则上,托管线程就是虚拟的;System.Threading.Thread.CurrentThread.ManagedThreadId返回的是托管线程ID,ID是稳定的;
  • System.AppDomain.GetCurrentThreadId()返回的是原生线程ID,ID是不稳定的。

【线程相关的一些老生常谈】

数据冲突 - 不一样步地去访问共享数据就会带来数据冲突,能够经过加锁来保护数据,可是锁的开销不小。

锁(Lock) - 不能是值类型,由于值类型在每次装箱时都是不一样的对象,因此根本没有办法保证锁的有效性。

死锁 - 两个或两个以上的线程在执行过程当中,竞争资源形成了阻塞。能够经过破坏他们产生的四个条件,或者利用诸如银行家算法等来避免。

考虑使用无锁的数据结构设计致使线程饿死 - 避免饿死就应该采用队列的方式,保证每一个线程都有机会得到请求的资源。 固然实现方式能够有不少变化,好比优先级、时间片等,都是“队列”的特殊形式。

【C# Job System】

写好线程安全的代码并不是易事,而C# Job System就用于帮助你们解决一切痛点,它将Unity底层的Job System开放给C#层的使用,因此具有Unity Job System的特性,基于任务式的高效安全的多线程和多任务之间能够存在依赖关系。

编码风格和接口声明:

除此之外C# Job System还提供了对线程安全相关的检查及报错提示机制,确保开发者在开发过程当中安全和高效。该项新功能即将与你们见面,请保持关注。

转自:
https://mp.weixin.qq.com/s?__biz=MjM5NjE1MTkwMg==&mid=2651042493&idx=1&sn=6f73f4dc1cc7b626f04afdd69169f948&chksm=bd1a858c8a6d0c9acb2a18009b126f88260f55e988566352dc15e685d0b12b9ab16f1a182961&mpshare=1&scene=24&srcid=0619iwjX8pc87aNKaMIqmxgC&key=e57780a9dd53e6fc93fd7e470425d9f45dae00b0c6853cf2b414e0e73f394687c4ea472b1879fe04b27dbd0eecbba30de1d20f260ba56ae1d54ca6bae4ef349aeadd74c18a359e35a3f0061fb5e36a43&ascene=0&uin=OTg2Mzc2Mzgw&devicetype=iMac+MacBookPro12%2C1+OSX+OSX+10.12.5+build(16F73)&version=12020610&nettype=WIFI&fontScale=100&pass_ticket=VD4kpJBlSF5Yd%2B3kVttZE0LSSP2dpPH4JrGSPxooYbHLdQc5XHzNJzK8oBihgj1u