本文主要介绍 Unity 中的协程(Coroutine)和 Yield 的关系,建议先阅读 Yield  了解 Yield 的基本概念。
Unity 协程 Unity 依赖 Yield  实现了异步编程的 协程(Coroutine)。
Yield  是 C# 提供的一种延迟获取一系列数据中每一个元素的方式:
yield 用来在 Iterator 中返回数据的关键字,yield return 和直接使用 return 不同的点在于它可以在返回数据的同时保持当前迭代器的状态并在下次对 Iterator 调用 MoveNext 时继续迭代器的执行,yield break 则是用来结束迭代器。C# Yield 关键字 
可以认为 yield 实现了 迭代器模式  ,它可以用来延迟数据的获取(例如针对一个集合,不需要一次性获得所有数据,而是在每一个数据真正使用时才获取)。C# Yield 关键字 
一个协程的示例如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 using  System.Collections;using  System.Threading;using  UnityEngine;  public  class  Coroutines  : MonoBehaviour {     private  void  Start ()     {         StartCoroutine(MySimpleCoroutine());     }       private  IEnumerator MySimpleCoroutine ()     {         Debug.Log("Hello from the coroutine!" );         Thread.Sleep(5000 );                  yield  break ;      } } 
当运行上述代码时,会发现 Unity 应用在 5 秒内无任何的相应,这是因为协程是运行在 Unity 的 UI 线程上(并非工作线程)的,而 Thread.Sleep(5000) 会阻塞 UI 线程,导致应用无响应。
协程的工作原理 可以看到上述的协程函数返回了一个 IEnumerator 对象,在 Yield 是如何工作的  中我们知道 yield 会在每次调用 IEnumerator 的 MoveNext 时返回一个值或终止迭代,并且会保持当前迭代器的状态。
Unity 中协程的实现原理即是在每一次调用 StartCoroutine 时,会将协程函数的 IEnumerator 对象加入到一个队列中,然后在每一帧调用 MoveNext 来执行协程函数,直到协程函数执行完毕,或调用 StopCoroutine 后将其从队列中移除。
自定义协程函数 可以通过在 Unity 中自定义一个 yield return 返回的数据类型来实现自定义的协程,以便验证上述协程的工作原理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public  class  WaitForTime  : IEnumerator {     private  readonly  DateTime m_EndTime;     public  WaitForTime (TimeSpan time )     public  bool  MoveNext ()     public  void  Reset ()     public  object  Current => null ; } public  class  CoroutineSample  : MonoBehaviour {     private  void  Start ()     private  IEnumerator WaitForTimeSample ()     {         Debug.Log("Enter WaitForTimeSample" );         yield  return  new  WaitForTime (TimeSpan.FromSeconds(5  ))         Debug.Log("Exit WaitForTimeSample" );     } } 
运行后,可以看到在打印了 Enter WaitForTimeSample 后,等待 5 秒后才打印了 Exit WaitForTimeSample,且在此期间 Unity 仍然可以响应其他的操作。
之所以如此,是因为 WaitForTime 类实现了 IEnumerator 接口,且在 MoveNext 中判断了是否到达了指定的时间,每一帧 Unity 都会调用 MoveNext 来判断是否继续执行协程函数,在时间不到目标时间时会返回 true 表示 MoveNext 仍然可以继续执行,而在时间到达目标时间后会返回 false 表示协程函数执行完毕。
进一步验证 如果进一步的,在 MoveNext 和 Current 中添加一些调试信息,可以看到在调用 StartCoroutine 时会触发一次 MoveNext,后续 Unity 每帧都会通过 SetupCoroutine 函数再次触发 MoveNext,以 MoveNext 为例子,调用堆栈为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 开启协程时调用 MoveNext Called MoveNext UnityEngine.Debug:Log (object) WaitForTime:MoveNext () (at Assets/Scripts/WaitForTime.cs:12) UnityEngine.MonoBehaviour:StartCoroutine (System.Collections.IEnumerator) CoroutineSample:Start () (at Assets/Scripts/CoroutineSample.cs:8) # 后续每帧都会d调用 MoveNext Called MoveNext UnityEngine.Debug:Log (object) WaitForTime:MoveNext () (at Assets/Scripts/WaitForTime.cs:12) UnityEngine.SetupCoroutine:InvokeMoveNext (System.Collections.IEnumerator,intptr) 
自定义协程机制 简易 Behaviour 实现 本节的展示内容因为涉及到要修改 Terminal 的标题,所以需要 .Net Tool,关于创建 .Net Tool 可以参考 创建 .Net Tools 
本节会尝试编写一个纯粹的 Console 应用,实现类似 Unity 的协程机制,即通过 yield return 来实现异步编程。首先我们通过 C# 实现极简版的 MonoBehaviour,及将 Console 标题修改的派生类 MySimpleBehaviour:
1 2 3 4 5 6 7 8 9 10 public  abstract  class  Behaviour {     public  virtual  void  Start ()     public  virtual  void  Update () } public  class  MySimpleBehaviour  : Behaviour {     public  override  void  Update ()$"Running at {1  / Time.deltaTime:F0}  FPS" ; } } 
其中 Time 是自定义的表示时间的类,其实现如下:
1 2 3 4 public  static  class  Time {     public  static  float  deltaTime { get ; internal  set ; } } 
主入口 Program 类需要触发所有的 MonoBehaviour 类的 Start 和 Update 函数,且需要记录每一次循环的时间差作为每一帧的 deltaTime:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 internal  static  class  Program {     private  static  readonly  List<Behaviour> s_Behaviours = [];     private  static  long  s_LastFrameTime = DateTime.Now.Ticks;     private  static  void  Main ()     {         s_Behaviours.Add(new  MySimpleBehaviour());         foreach  (Behaviour behaviour in  s_Behaviours)         {             behaviour.Start();         }         while  (true )         {             long  frameTime = DateTime.Now.Ticks;             long  deltaTime = frameTime - s_LastFrameTime;             s_LastFrameTime = frameTime;             Time.deltaTime = (float ) TimeSpan.FromTicks(deltaTime).TotalSeconds;             foreach  (Behaviour behaviour in  s_Behaviours)             {                 behaviour.Update();             }             Thread.Sleep(10 );          }     } } 
此时运行结果为:
增加 Coroutine 机制 在 Behavior 的基类中提供函数 StartCoroutine,其实现逻辑为将 IEnumerator 对象加入到一个队列中,然后在每一帧调用 MoveNext 来执行协程函数,直到协程函数执行完毕:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public  abstract  class  Behaviour {     private  readonly  List<IEnumerator> m_ActiveCoroutines = new ();     public  virtual  void  Start ()     public  virtual  void  Update ()     protected  void  StartCoroutine (IEnumerator coroutine )     internal  void  UpdateCoroutines ()     {         for  (int  i = m_ActiveCoroutines.Count - 1 ; i >= 0 ; i--)         {             IEnumerator coroutine = m_ActiveCoroutines[i];             if  (!coroutine.MoveNext())             {                 m_ActiveCoroutines.RemoveAt(i);             }         }     } } 
在 Program 中,在每一帧再额外调用 UpdateCoroutines :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 internal  static  class  Program {              while  (true )         {                  foreach  (Behaviour behaviour in  s_Behaviours)             {                 behaviour.Update();                 behaviour.UpdateCoroutines();             }             Thread.Sleep(10 );          }      } 
此时再拓展 MySimpleBehaviour 类,在其中调用 StartCoroutine 来启动定义的协程 Foo,Bar,而 Foo 会再嵌入Foo2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public  class  MySimpleBehaviour  : Behaviour {     public  override  void  Start ()     {         base .Start();                  StartCoroutine(Foo());         StartCoroutine(Bar());     }     public  override  void  Update ()$"Running at {1  / Time.deltaTime:F0}  FPS" ; }     private  IEnumerator Foo ()     {         yield  return  Foo2 ()         Console.WriteLine("Foo Finished!" );     }     private  IEnumerator Foo2 ()     {         for  (int  i = 0 ; i < 5 ; i++)         {             Console.WriteLine(i);             yield  return  new  WaitForSeconds (1          }         Console.WriteLine("Foo2 Finished!" );     }     private  IEnumerator Bar ()     {         yield  return  new  WaitForSeconds (2          Console.WriteLine("Bar Finished!" );     } } 
此时运行程序会发现,立即就打印出了 Foo Finished!,和 Bar Finished!,并没有预期的等待时间,且没有预期的打印出 Foo2 Finished!。这是因为我们在 StartCoroutine 时只是将最根结点的 IEnumerator 添加进了队列,在 UpdateCoroutine 中也仅仅对这些根节点的 IEnumerator 调用了 MoveNext, 被 yield return new 所构建的 IEnumerator 则没有被处理。
针对这个问题,我们需要做的就是对调用 StartCoroutine 的 IEnumerator 管理其整个调用栈,而不是仅仅是根节点。修改 Behavior 函数如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public  abstract  class  Behaviour {     private  readonly  List<Stack<IEnumerator>> m_ActiveCoroutines = new ();     public  virtual  void  Start ()     public  virtual  void  Update ()     protected  void  StartCoroutine (IEnumerator coroutine )     {         m_ActiveCoroutines.Add(new  Stack<IEnumerator>(new [] {coroutine}));     }     internal  void  UpdateCoroutines ()     {         for  (int  index = 0 ; index != m_ActiveCoroutines.Count; ++index)         {             Stack<IEnumerator> instructions = m_ActiveCoroutines[index];             if  (instructions.Count == 0 )             {                                  m_ActiveCoroutines.RemoveAt(index);                                  index--;             }             else              {                 IEnumerator instruction = instructions.Peek();                 if  (instruction.MoveNext())                 {                     if  (instruction.Current is  IEnumerator nestedInstruction)                     {                         instructions.Push(nestedInstruction);                     }                 }                 else                  {                     instructions.Pop();                 }             }         }     } } 
其中将对 IEnumerator 的管理变为了 Stack<IEnumerator>,在调用 StartCoroutine 时,将根节点的 IEnumerator 加入到 Stack 中作为第一个元素。
UpdateCoroutines 的函数改变较大,其核心语句为:
1 2 3 4 5 6 7 if  (instruction.MoveNext()){     if  (instruction.Current is  IEnumerator nestedInstruction)     {         instructions.Push(nestedInstruction);     } } 
其中的关键是检查当前 IEnumerator 的 Current 是否为 IEnumerator,如果是则说明存在嵌套的 IEnumerator(由 yield return new 产生的),则将其加入到 Stack 中,以便在下一次调用 MoveNext 时处理。
如果 instruction.MoveNext() 返回 false,则说明当前 IEnumerator 执行完毕,通过 instructions.Pop() 将其从 Stack 中移除。
1 2 3 4 5 6 7 8 if  (instruction.MoveNext()){      } else {     instructions.Pop(); } 
当一个 Stack 中的所有 IEnumerator 执行完毕后,将其从 List 中移除,其中 index-- 是为了避免在循环中移除元素导致遗漏下一个元素:
1 2 3 4 5 6 7 if  (instructions.Count == 0 ){          m_ActiveCoroutines.RemoveAt(index);          index--; } 
MoveNext 的执行频率是每一帧( Thread.Sleep(10)) 控制的,因此这里的 WaitForSeconds 会有一定的误差,但是整体的执行逻辑是正确的。
Reference How do Unity’s coroutines actually work? - Oliver Booth 
Fetching Title#8pok