在进入本章主题之前,我们必须要了解客户端应用程序都是单线程模型,即只有一个主线程(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应用程序而言,还提供了另外一种『异步方式』:CoroutineCoroutine也就是协程的意思,只是看起来像多线程,它实际上并不是,还是在主线程上操作。

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=-1
while (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,MoveNextReset步骤。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)的更多相关文章

  1. Unity应用架构设计(10)——绕不开的协程和多线程(Part 2)

    在上一回合谈到,客户端应用程序的所有操作都在主线程上进行,所以一些比较耗时的操作可以在异步线程上去进行,充分利用CPU的性能来达到程序的最佳性能.对于Unity而言,又提供了另外一种『异步』的概念,就 ...

  2. Unity应用架构设计(10)————绕不开的协程和多线程(Part 1)

    在进入本章主题之前,我们必须要了解客户端应用程序都是单线程模型,即只有一个主线程(Main Thread),或者叫做UI线程,即所有的UI控件的创建和操作都是在主线程上完成的.而服务器端应用程序,也就 ...

  3. 关于Unity中协程、多线程、线程锁、www网络类的使用

    协程 我们要下载一张图片,加载一个资源,这个时候一定不是一下子就加载好的,或者说我们不一定要等它下载好了才进行其他操作,如果那样的话我就就卡在了下载图片那个地方,傻住了.我们希望我们只要一启动加载的命 ...

  4. 学习PYTHON之路, DAY 10 进程、线程、协程篇

    线程 线程是应用程序中工作的最小单元.它被包含在进程之中,是进程中的实际运作单位.一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务. 直接调用 impo ...

  5. UNITY所谓的异步加载几乎全部是协程,不是线程;MAP3加载时解压非常慢

    实践证明,以下东西都是协程,并非线程(thread): 1,WWW 2,AssetBundle.LoadFromFileAsync 3,LoadSceneAsync 其它未经测试 此问题的提出是由于一 ...

  6. Unity应用架构设计(11)——一个网络层的构建

    对于客户端应用程序,免不了和远程服务打交道.设计一个良好的『服务层』能帮我们规范和分离业务代码,提高生产效率.服务层最核心的模块一定是怎样发送请求,虽然Mono提供了很多C#网络请求类,诸如WebCl ...

  7. Unity应用架构设计(13)——日志组件的实施

    对于应用程序而言,日志是非常重要的功能,通过日志,我们可以跟踪应用程序的数据状态,记录Crash的日志可以帮助我们分析应用程序崩溃的原因,我们甚至可以通过日志来进行性能的监控.总之,日志的好处很多,特 ...

  8. Unity应用架构设计(6)——设计动态数据集合ObservableList

    什么是 『动态数据集合』 ?简而言之,就是当集合添加.删除项目或者重置时,能提供一种通知机制,告诉UI动态更新界面.有经验的程序员脑海里迸出的第一个词就是 ObservableCollection.没 ...

  9. Unity应用架构设计(1)—— MVVM 模式的设计和实施(Part 2)

    MVVM回顾 经过上一篇文章的介绍,相信你对MVVM的设计思想有所了解.MVVM的核心思想就是解耦,View与ViewModel应该感受不到彼此的存在. View只关心怎样渲染,而ViewModel只 ...

随机推荐

  1. python——比return优先级更高的语句

    调用sqlmap,使用sqlmap做二次开发的时候,出现的问题: 在调用sqlmap中return,然而主程序还是会被sqlmap中的某些代码给中断. 添加try也无法阻止中断. 后来猜测中断是由ex ...

  2. Jmeter压测过程报错the target server failed to respond

    失败事务报错信息如下, Socket closed Non HTTP response code: org.apache.http.NoHttpResponseException (the targe ...

  3. 一道颇有难度的JavaScript题

    上次分享了一道题,大家反响不错,很开心自己写的东西有人愿意花时间去看,也给了自己莫大的鼓舞,其实做题虽然不比真正的编程,但是也能够让你发现一些你之前没有注意到的语言层面的问题.所以,这次再分享一道稍微 ...

  4. 码云,git使用 教程

    码云,git使用 教程 code cloud, git use tutorials 作者:韩梦飞沙 Author:han_meng_fei_sha 邮箱:313134555@qq.com E-mail ...

  5. mongoose根据_id更新,且如果数组中没有元素就添加

    await model.photo.update({ _id: { $in: photoIdsParam } }, { $addToSet: { customerIds: { code: custCo ...

  6. Debian stretch更换国内源

    在debian图形化安装过程中就可以选择网络镜像的位置 据说电信用清华的源快,移动的用网易源快 备份源配置文件: cp /etc/apt/sources.list /etc/apt/sources.l ...

  7. (转)nginx uwsgi wsgi django 这些东西究竟是什么关系

    有太多的文章告诉我们nginx uwsgi django 这些东西怎么用了,太多的人知道这些东西的怎么使用,怎么配置,怎么优化,但是还是有一部分人比如我这种水货不知道这些东西到底是啥,为啥一个项目的发 ...

  8. 创建触发器(trigger)

    创建触发器 DROP TRIGGER IF EXISTS `ins_table_name`; DELIMITER ;; CREATE TRIGGER `ins_table_name` AFTER IN ...

  9. IO流(4)—字符流

    1.IO体系: 抽象基类 --节点流(文件流) InputStream -- FileInputStream OutputStream --FileOutputSteam Reader --FileR ...

  10. Tornado-cookie

    cookie 服务端在客户端的中写一个字符串,下一次客户端再访问时只要携带该字符串,就认为其是合法用户. tornado中的cookie有两种,一种是未加密的,一种是加密的,并且可以配置生效域名.路径 ...