• 协程(Coroutine)是Unity中一个让代码异步执行的一种机制,协程之间是单线程执行的,同时他的特点是能让程序员用yield主动挂起,并且显式地控制唤醒的时机。
  • 通过协程可以实现计时器、将耗时的操作拆分成几个步骤分散在每一帧去运行等等。
  • 协同程序与线程差不多,也就是一条执行序列,拥有自己独立的栈,局部变量和指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。
  • 线程与协同程序的主要区别在于,一个具有多线程的程序可以同时运行几个线程,而协同程序却需要彼此协作地运行。
  • 就是说,一个具有多个协同程序的程序在任何时刻只能运行一 个协同程序,并且正在运行的协同程序只会在其显示地挂起时,它的执行才会暂停。

协程的使用

  • StartCoroutine:开始协程
  • StopCoroutine:停止协程
  • 协程中要以IEnumerator为返回值,通过yield来暂停协程的执行,同时说明下次开启的条件
  • 协同程序主要是在Update()方法之后,LateUpdate()方法之前调用。
  • 协程停止和创建必须要用同样的形式来进行
    • 参数中写字符串的形式
    • 参数传IEnumerater类型对象的形式
    • 参数传Coroutine类型对象的形式
  • Start可以为IEnumerator类型,成为协程的形式,然后可以在start中yield等待任何一个IEnumerator为返回值的函数执行

协程、Invoke、InvokeRepeating的执行区别

实验1:禁用对应脚本

Coroutine:执行

Invoke:执行

InvokeRepeating:执行

实验2:禁用对应的gameobject

Coroutine:不执行

Invoke:执行

InvokeRepeating:执行

实验3:销毁对应的脚本

Coroutine:不执行

Invoke:不执行

InvokeRepeating:不执行

实验4:销毁对应的gameobject

Coroutine:不执行

Invoke:不执行

InvokeRepeating:不执行

底层实现

  • 协程其本质其实是通过IEnumerator迭代器实现的一种状态机,故其本质还是单线程的,一旦协程卡住整个线程也会卡住。
  • Unity实际上是可以对所有的继承自YieldInstruction的类去使用协程的,也就是说Yied关键字对所有的YieldInstruction继承的类都是生效的。
  • 关于C#迭代器的实现,可参考这个文章
  • IEnumerator迭代器:
    • //迭代器接口
      public interface IEnumerator
      {
          //Current属性为只读属性,返回枚举序列中的当前位的内容
          object Current { get; }
          //MoveNext()把枚举器的位置前进到下一项,返回布尔值,新的位置若是有效的,返回true;否则返回false
          bool MoveNext();
          //Reset()将位置重置为原始状态
          void Reset();
      }
      
      
    • //一个迭代器的例子
      class B
      {
          public IEnumerator enumerableFuc()
          {
              int i = 0;
              Console.WriteLine("Enumerator " + i);
              yield return i;
              i++;
              Console.WriteLine("Enumerator " + i);
              yield return i;
              i++;
              Console.WriteLine("Enumerator " + i);
              yield break;
          }
      }
      
      class Program
      {
          static void Main(string[] args)
          {
              B b = new ();
              IEnumerator enumerator = b.enumerableFuc();
              while (enumerator.MoveNext())
              {
                  Console.WriteLine("main " + enumerator.Current);
                  Console.WriteLine("Enumerator Run");
              }
          }
      }
      
    • 以上的例子可以看出enumerableFuc方法被截成了3个部分,通过MoveNext()返回的bool来判断迭代器是否走完,而yield return的值则通过Current属性返回。
    • 整个过程可以理解为当我们调用IEnumerator enumerator = b.enumerableFuc();时,enumerableFuc方法会根据yield关键字将整个方法拆分成各个代码块。
    • yield关键字是一个语法糖,背后其实生成了一个新的枚举器类。
    • yield return的作用是在执行到这行代码之后,将控制权立即交还给外部。
    • yield return之后的代码会在外部代码再次调用MoveNext时才会执行,然后执行下一个yield return或是迭代结束。
    • yield break语法块会直接返回一个MoveNext 返回值为false的枚举器类,从而终止迭代器运行。

自己实现一个协程

class CoroutinesManager
{
    public List<IEnumerator> coroutines = new List<IEnumerator>();
    //模仿协程开始,将协程加入到协程的执行列表中
    public void StartCoroutine(IEnumerator coroutine)
    {
        coroutines.Add(coroutine);
    }
}
class B 
{
    public IEnumerator enumerableFuc1()
    {
        int i = 0;
        Console.WriteLine("Enumerator1 " + i);
        yield return i;
        i++;
        Console.WriteLine("Enumerator1 " + i);
        yield return i;
        i++;
        Console.WriteLine("Enumerator1 " + i);
        yield break;
    }
  
    public IEnumerator enumerableFuc2()
    {
        int i = 100;
        Console.WriteLine("Enumerator2 " + i);
        yield return i;
        i++;
        Console.WriteLine("Enumerator2 " + i);
        yield return i;
        i++;
        Console.WriteLine("Enumerator2 " + i);
        yield break;
    }
}
class Program
{   
    static void Main(string[] args)
    {
        int i = 0;
        CoroutinesManager coroutinesManager = new CoroutinesManager();
        B b = new();
        coroutinesManager.StartCoroutine(b.enumerableFuc1());
        coroutinesManager.StartCoroutine(b.enumerableFuc2());
  
        //模仿Unity生命周期循环(一帧)
        while (true)
        {
            Console.WriteLine("frame: " + ++i);
            if (coroutinesManager.coroutines.Count <= 0)
            {
                return;
            }
            List<IEnumerator> delete = new List<IEnumerator>();
            foreach (var item in coroutinesManager.coroutines)
            {
                if (!item.MoveNext())
                {
                    delete.Add(item);
                }
            }
            foreach (var item in delete)
            {
                coroutinesManager.coroutines.Remove(item);
            }
        }
    }
}

注意:unity中的协程比我们实现的更加复杂,包括协程的嵌套、调度器,计时器实现、停止都没有实现。

  • unity为了实现协程在干的事情其实就是支持了一堆yield return 的返回值, 再写一个调度器,根据返回值来按时候唤醒(调用.MoveNext())。
  • 比如你yield return new WaitForSeconds(1), 在返回后调度器就每一帧来看一下,这个函数离上次yield的时候有1秒了没,如果到1秒了,就调用迭代器的.MoveNext()。
  • 调度器的运行细节:
    • 首先每次的StartCoroutine()其实就是在调度系统里“注册”了这个协程。
    • 当你编写了一个带yield return的协程函数的时候,C#编译器会这个函数生成一个类。
    • 当你发起一个协程的时候,就会生成一个Coroutine对象。
    • 这个Coroutine对象用来保存追踪这个协程的执行状态,比如当前本地变量的值,yield的状态。
    • 因此协程是会带来一定程度的内存开销以及GC。
  • Unity协程中yield return支持的协程响应类有以下这些:
    • //下一帧执行后续代码
      yield return null;
      //下一帧执行后续代码
      yield return 数字;
      //结束迭代器
      yield break;
      //Unity用于进行基于协程的异步操作的基类
      yield return new AsyncOperation();
      //协程的嵌套
      yield return IEnumerator;
      //用于协程的嵌套和监听Coroutine类是StartCoroutine返回的协程句柄
      yield return new Coroutine();
      //WWW继承CustomYieldInstruction,CustomYieldInstruction可以用于实现自定义协程响应类
      yield return new WWW();
      //在这帧结束,在 Unity 渲染每一个摄像机和 GUI 之后,在屏幕上显示下一帧之前执行后续代码。
      yield return new WaitForEndOfFrame();
      //下一帧FixedUpdate开始时执行后续代码
      yield return new WaitForFixedUpdate();
      //延时设置的等待时间之后一帧的Update执行完成后运行,受到Time.timeScale影响
      yield return new WaitForSeconds(等待时间/秒);
      //延时设置的等待时间之后一帧的Update执行完成后运行
      yield return new WaitForSecondsRealtime(等待时间/秒);
      

协程与Invoke的区别

  • Invoke函数必须输入方法名,对于上下文变量、属性的引用就会尤为不便,因此不能传有参方法进Invoke
  • Invoke执行没有被挂起,相当于设置完被调用函数的执行时间后即时向下执行
  • 协程新开一条执行序列(跟新建线程差不多)并挂起,等待中断指令结束
  • 协程相比Invoke拥有更多参数选择,比如等待一帧后执行,等待某个函数执行完毕后执行等
  • 协程的效率比Invoke高

协程的缺点

  • 依赖于MonoBehaviour,比如我们在MVC开发时,如果M、C层不继承MonoBehaviour,也就无法使用协程
  • 因为返回值为IEnumerator,所以不能有其他返回值
  • 如果要顺序的完成一些任务,使用协程会造成回调地狱(回调函数里面嵌套回调函数)
  • 如何避免:
    • 自定义一个调度器(不依赖于MonoBehaviour)
    • 使用异步Async/Await(避免回调地域)
    • 使用UniTask来进行异步编程(详见后面的参考资料)

协程、进程、线程的区别

全网找的应该是讲的最清楚的文章

进程:

  • 保存在硬盘上的程序运行以后,会在内存空间里形成一个独立的内存体,这个内存体有自己独立的地址空间,有自己的堆,上级挂靠单位是操作系统。
  • 一个应用程序相当于一个进程,操作系统会以进程为单位,分配系统资源(CPU 时间片、内存等资源),进程是资源分配的最小单位。

线程:

  • 线程从属于进程,是程序的实际执行者。线程是操作系统能够进行运算调度的最小单位。
  • 它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。一个线程只有一个进程。
  • 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。

协程:

  • 协程是伴随着主线程一起运行的一段程序。意点:
  • 协程与协程之间是并行执行,与主线程也是并行执行,同一时间只能执行一个协程提起协程,自然是要想到线程,因为协程的定义就是伴随主线程来运行的!
  • 一个线程可以拥有多个协程,协程不是被操作系统内核所管理,而完全是由程序所控制。
  • 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。

参考资料

Unity 协程底层原理解析

Unity协程的实现原理

深入看一看Unity协程Coroutine

浅析Unity协程实现原理

Unity 协程的原理

C# 中的Async 和 Await 的用法详解

UniTask使用笔记

对C#中async和await的理解

Unity中的异步编程【1】—— Unity与async 、 await

Unity3d_协程和Invoke二者的区别

【Unity】Unity 进程、线程、协程