是否需要多线程?

这是个好问题,从设备的硬件上,这已不是瓶颈:

学过操作系统的同学肯定知道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中使用和管理多线程。

https://mp.weixin.qq.com/s/4GxmlVCb8SGS_U23q2xlgA