Unity应用架构设计(10)——绕不开的协程和多线程(Part 1)
阅读目录
- 是否需要多线程?
- 协程的内部原理
- 小结
在进入本章主题之前,我们必须要了解客户端应用程序都是单线程模型,即只有一个主线程(Main Thread),或者叫做UI线程,即所有的UI控件的创建和操作都是在主线程上完成的。而服务器端应用程序,也就是我们常见的Web应用程序往往是多线程的,故用户A访问势必不会影响用户B的访问过程。所以对于Web应用而言,多线程的数据同步和并发的管理往往是个头疼的问题。那么对于客户端应用程序而言,就一个人使用,还要需要考虑多线程吗?
是否需要多线程?
这是个好问题,从设备的硬件上,这已不是瓶颈:
学过操作系统的同学肯定知道CPU是真正的处理大脑,在单核的CPU年代,在某一时刻CPU只能处理一个线程,通过CPU的调度来实现在不同线程间切换工作。由于CPU调度的时间很快,所以给人造成并发的假象。
随着硬件的提升,多核CPU已经是常态化了。比如双核CPU而言,某一时刻可以有2个线程并行计算。
所以,是否需要在客户端使用多线程技术,还是取决于你的应用的复杂度:
- 如果你的应用不需要一些耗时的操作,比如网络请求,IO操作,AI等,那么尽量不要使用多线程,因为跨线程访问UI控件是禁止的,并且数据同步问题往往也是很棘手的,很容易滥用
lock
导致主线block或者deadlock。 - 反之,如果应用程序很复杂,那么势必在需要去分担主线程的压力,那么使用异步线程是个很好的主意。
- 同时,我们也不能滥用线程,过多的使用线程会造成CPU运算的下降,建议使用线程池
ThreadPool
或者利用GC来回收线程。
协程的内部原理
回到本文的主题,对于Unity应用程序而言,还提供了另外一种『异步方式』:Coroutine
。Coroutine
也就是协程的意思,只是看起来像多线程,它实际上并不是,还是在主线程上操作。
Coroutine实际上由IEnumerator
接口以及一个或者多个的yield
语句构成的迭代器(iterator
)块构成。
枚举器接口 IEnumerator
包含3个方法:
- Current:返回集合当前位置的对象
- MoveNext:把枚举器位置移到集合的下一个元素,它返回一个bool值,表示新的位置是否超过索引
- Reset:把位置重置为初始状态
yield
是个比较晦涩的技术,原因是编译器帮我们做了太多的工作(CompilerGenerate),导致我们无法理解到内部的实现。如果你去翻阅汉英词典,你会对yield一头雾水。我个人倾向将其翻译成中断和产出比较好,这也是yield单词包含的意思,我下面也会阐述为什么要翻译成这两个意思。
深究yield
之前,我觉得应该略微了解一下为什么我们能foreach
遍历一个数组?
原因很简单,数组Array它是一个可枚举的类(enumerable),一个可枚举类提供了一个枚举器(enumerator),枚举器可以依次访问数组里的元素,也就是之前提过的
Current
属性返回集合当前位置的对象。所以,我可以模拟foreach
的实现,实际上foreach
内部实现也大致相似。
static void Main(string[] args)
{string[] animals = {"dog", "cat", "pig"};//获取枚举器var ie = animals.GetEnumerator();//移到下一项,默认的index=-1while (ie.MoveNext()){//获得当前项Console.WriteLine(ie.Current);}Console.ReadLine();
}
假设你是个C#新手,你得好好消化一下上述的逻辑,因为这是拨开迷雾的第一层:了解为什么能够枚举一个集合。当然我们也可以创建自己的可被枚举的类,需要为它提供自定义的枚举器,只需实现IEnumerator
接口即可。值得注意的事,自建的可枚举类同时也要实现IEnumerable
接口,该接口只提供一个方法:GetEnumerator()
,用来返回枚举器。
创建自定义的枚举类AnimalSet
:
class AnimalSet : IEnumerable
{private readonly string[] _animals = {"the dog", "the pig", "the cat"};public IEnumerator GetEnumerator(){return new AnimalEnumerator(_animals);}
}
需要为AnimalSet
提供自定义的枚举器AnimalEnumerator
class AnimalEnumerator : IEnumerator
{private string[] _animals;private int _index = -1;public AnimalEnumerator(string[] animals){_animals=new string[animals.Length];for (var i = 0; i < animals.Length; i++){_animals[i] = animals[i];}}public bool MoveNext(){_index++;return _index<_animals.Length;}public void Reset(){_index = -1;}public object Current{get { return _animals[_index]; }}
}
你可能会觉得奇怪,这和yield
又有什么关系呢?要解惑yield
这是第二个阶段:能知道枚举器是怎样工作的。
如果你很清楚上诉两个阶段的内部原理之后,要理解Unity中的Coroutine
是非常简单的,你会了解为什么它是伪的“多线程”。
这是一段非常普通的代码,司空见惯。
void Start()
{StartCoroutine(MyEnumerator());Debug.Log("finish");
}private IEnumerator MyEnumerator()
{Debug.Log("wait for 1s");yield return new WaitForSeconds(1);Debug.Log("wait for 2s");yield return new WaitForSeconds(2);Debug.Log("wait for 3s");yield return new WaitForSeconds(3);
}
注意到MyEnumerator
方法的放回类型了吗?没错,返回的就是枚举器,你会疑问,你没有定义一个枚举器并且实现了IEnumerator
接口啊!别急,问题就出在yield
上,C#为了简化我们创建枚举器的步骤,你想想看你需要先实现IEnumerator
接口,并且实现Current
,MoveNext
,Reset
步骤。C#从2.0开始提供了有yield
组成的迭代器块。编译器会自动更具迭代器块创建了枚举器。不信,反编译看看:
public class Test : MonoBehaviour
{private IEnumerator MyEnumerator(){UnityEngine.Debug.Log("wait for 1s");yield return new WaitForSeconds(1f);UnityEngine.Debug.Log("wait for 2s");yield return new WaitForSeconds(2f);UnityEngine.Debug.Log("wait for 3s");yield return new WaitForSeconds(3f);}private void Start(){base.StartCoroutine(this.MyEnumerator());UnityEngine.Debug.Log("finish");}[CompilerGenerated]private sealed class <MyEnumerator>d__1 : IEnumerator<object>, IEnumerator, IDisposable{private int <>1__state;private object <>2__current;public Test <>4__this;[DebuggerHidden]public <MyEnumerator>d__1(int <>1__state){this.<>1__state = <>1__state;}private bool MoveNext(){switch (this.<>1__state){case 0:this.<>1__state = -1;UnityEngine.Debug.Log("wait for 1s");this.<>2__current = new WaitForSeconds(1f);this.<>1__state = 1;return true;case 1:this.<>1__state = -1;UnityEngine.Debug.Log("wait for 2s");this.<>2__current = new WaitForSeconds(2f);this.<>1__state = 2;return true;case 2:this.<>1__state = -1;UnityEngine.Debug.Log("wait for 3s");this.<>2__current = new WaitForSeconds(3f);this.<>1__state = 3;return true;case 3:this.<>1__state = -1;return false;}return false;}object IEnumerator.Current{[DebuggerHidden]get{return this.<>2__current;}}//...省略...}
}
有几点可以确定:
yield
是个语法糖,编译过后的代码看不到yield
- 编译器在内部创建了一个枚举类
<MyEnumerator>d__1
yield return
被声明为枚举时的下一项,即Current属性,通过MoveNext方法来访问结果
OK,通过层层推进,想必你对Untiy中的协程有一定的了解了。再回过头来,我将yield
翻译成了中断和产出,谈谈我的理解。
- 中断:传统的方法代码块执行流程是从上到下依次执行,而
yield
构成的迭代块是告诉编译器如何创建枚举器的行为,反编译得到的结果可以看到,它们的执行并不是连续的,而是通过switch
来从一个状态(state)跳转到另一个状态 - 产出:
yield
是和return
连用,yield return
之后的语句被编译器赋值给current变量,最终通过Current
属性产出枚举项
小结
本文的初衷是想介绍如何在Unity中使用多线程,但协程往往是绕不开的话题,于是索性就剖析了下它,故决定单独成一篇。本章内容对多线程开了个头,我将在下篇文章中说说怎样在Unity中使用和管理多线程。
源代码托管在Github上,点击此了解
Unity应用架构设计(10)——绕不开的协程和多线程(Part 1)相关推荐
- 视频教程-Unity网络游戏架构设计-Unity3D
Unity网络游戏架构设计 网名:海洋,CSDN社区讲师,3D游戏引擎开发者,IT讲师,计算机图形学方向研究生,曾在浙江大学CAD&CG;国家重点实验室学习.从事IT行业15年,主导或参与了1 ...
- UNITY所谓的异步加载几乎全部是协程,不是线程;MAP3加载时解压非常慢
UNITY所谓的异步加载几乎全部是协程,不是线程;MAP3加载时解压非常慢 实践证明,以下东西都是协程,并非线程(thread): 1,WWW 2,AssetBundle.LoadFromFileAs ...
- 10分钟 GET 新技能 - 协程
10分钟 GET 新技能 - 协程 1. 协程 是什么? 2. 协程 的基本规则 3. Python 流行库 gevent 中使用 协程 10分钟 GET 新技能 - 协程 1. 协程 是什么? 协程 ...
- 简单Unity时间架构设计(克洛诺斯之匙)
好吧,这次的题目有点标题党之嫌,提出这个设计,是因为最近玩了鬼泣,其中有一个关卡叫做"为了自己的主人",任务中,需要利用克洛诺斯之匙将时间变慢,便于通过激光镇. 使用克洛诺斯之匙之 ...
- 大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端
前言:苟有恒,何必三更眠五更起:最无益,莫过一日曝十日寒. 前言 之前一直想写个 WanAndroid 项目来巩固自己对 Kotlin+Jetpack+协程 等知识的学习,但是一直没有时间.这里重新行 ...
- Unity应用架构设计(11)——一个网络层的构建
对于客户端应用程序,免不了和远程服务打交道.设计一个良好的『服务层』能帮我们规范和分离业务代码,提高生产效率.服务层最核心的模块一定是怎样发送请求,虽然Mono提供了很多C#网络请求类,诸如WebCl ...
- Unity应用架构设计(6)——设计动态数据集合ObservableList
什么是 『动态数据集合』 ?简而言之,就是当集合添加.删除项目或者重置时,能提供一种通知机制,告诉UI动态更新界面.有经验的程序员脑海里迸出的第一个词就是 ObservableCollection.没 ...
- Unity应用架构设计(1)—— MVVM 模式的设计和实施(Part 2)
MVVM回顾 经过上一篇文章的介绍,相信你对MVVM的设计思想有所了解.MVVM的核心思想就是解耦,View与ViewModel应该感受不到彼此的存在. View只关心怎样渲染,而ViewModel只 ...
- Unity应用架构设计(9)——构建统一的 Repository
谈到 『Repository』 仓储模式,第一映像就是封装了对数据的访问和持久化.Repository 模式的理念核心是定义了一个规范,即接口『Interface』,在这个规范里面定义了访问以及持久化 ...
最新文章
- [Jarvis OJ - PWN]——[XMAN]level2
- npm更换成淘宝镜像源以及cnpm
- SOA架构师注意的问题
- SendMessage、PostMessage原理和源代码详解
- 1、kali的安装与环境配置
- cad怎么快速算面积_微信收到CAD图纸打不开怎么办?2种方法教你手机CAD快速看图...
- visual stadio.net已检测到web服务器运行的不是asp.net1.1版故障的排除
- VM ESXI安装英伟达显卡驱动
- H264的NAL单元详解
- Vue和Springboot实现SM4加密和解密(前端可加密,后端可加解密,MD5同理)
- 【预测模型-随机森林分类】基于随机森林算法实现数据分类附matlab代码
- 适用于 Windows 10 的触摸板手势
- Autosar诊断——故障诊断方案设计
- QQ个性装扮气泡免费使用
- 【项目评级】ArcBlock(ABT)-区块链基石网络
- SQL Server中,备份数据表
- 我的世界服务器怎么弄领地语言,我的世界领地指令大全 教你如何设置领地
- [DirectX]Assimp环境配置
- 四级资料免费分享 【写作万能模板 + 听力高频词 + 核心500词 + 翻译必备句型 + 作文对策】 点个关注即可全部拿走!!!
- 使用python暴力破解zip压缩包的密码
热门文章
- adobe stream的最后一行空行_玩转Java8Stream(五、并行Stream)
- php 列表 单击事件,首页gt; PHPgt;如何添加点击事件到jstree的(jQuery插件)异步列表?...
- plt python 画直线_Matplotlib:先搞明白plt. /ax./ fig再画
- 思源EMLOG文章页网址跳转插件V1.1
- HTML+CSS物业后台管理系统模板
- PHP源码-2021来客多商户在线客服系统
- 极简好看的个人介绍页源码
- vs2010 在svn上传东西的时候 出现一个错误 An internal occurred 值不在预期范围内 要如何解决呢?...
- Linux 死机了怎么办
- 实现动态验证码的思路