协程的原理自己基本看一遍忘一遍,因此想自己整理一些资料好让资料能留一些在脑子里面

协程

Unity的协程是为了解决“游戏运行中有些物体需要在不同帧异步加载出来”这一问题而被设计出来的,其基本规则是使得协程中的规定的代码能分片到unity运行中的不同帧中去执行。

用法

关于协程的用法请看Unity官方文档,使用问题不做赘述

原理分析

【前置知识】Part1 迭代器

这部分懂了可以直接跳过看Part2

在使用协程的时候,我们总是要声明一个返回值为IEnumerator的函数,并且函数中会包含yield return xxx或者yield break之类的语句。就像文档里写的这样

private IEnumerator WaitAndPrint(float waitTime)
{
        yield return new WaitForSeconds(waitTime);
        print("Coroutine ended: " + Time.time + " seconds");
}

IEnumerator,yield是C#语言中迭代器功能中的关键字。
迭代器是C#一种遍历集合的机制,通过开放IEnumerable接口或者实现了GetEnumerator()方法就可以使用foreach去遍历类,遍历输出的结果是根据GetEnumerator()的返回值IEnumerator确定的

迭代器关键理解

迭代器设计思想是在使用光标去遍历集合对象,光标所指就是当前集合的所指的对象

关键词:

  • IEnumerable: 需要遍历的集合使用这个接口,标志他是需要迭代访问的素材
  • GetEnumerator: IEnumerable里面定义的方法,也可以在不继承IEnumerable直接显示声明,作用就是返回一个IEnumerator用于遍历集合(后续会结合迭代器本质进行解释)
  • IEnumerator: 迭代器接口,用于遍历自定义集合,有三个方法声明:
  • Current():获取当前对象
  • MoveNext():判断迭代器下一个是否为空,否就把迭代器指向对象指向下一个集合元素
  • Reset():重置迭代器光标
  • yield:迭代器的语法糖,稍后解释
迭代器的本质

为了使用foreach (var item in colletion)语句,流程如下:

  1. 先执行 in 后面的 colletion.GetEnumerator 获取迭代器
  2. 执行获取到的迭代器的 MoveNext 方法
  3. 执行到的MoveNext如果为 True, 说明光标指向了合法对象,通过Current()返回item

因此不难看出,我们可以自己手写foreach代码,只需要用while循环不断地调用MoveNext然后在循环中使用Current()就行。

yield 语法糖

yield 关键字在写法上简化了MoveNext() + Current()操作,不用看一大串代码了,具更深入请看下面的代码模板

迭代器基本代码模板

//类声明-----------------------------------------------------------------
//标准版本
class CustomList : IEnumerable, IEnumerator
{
    private int[] list;
    private int index = -1;
    public CustomList() 
    { 
        list = new int[] 
        {
            1, 2, 3, 4, 5, 6, 7, 8 
        }; 
    }
    public CustomList(params int[] list)
    {
        this.list = list;
    }
	
	//IEnumerable 规定实现
    public IEnumerator GetEnumerator()
    { 
        Reset();
		//因为该类也继承IEnumerator,可以返回this
        return this; 
    }
	
	//IEnumerator 规定实现
    public object Current
    {
        get
        {
            return list[index]; 
        } 
    }
	
	//IEnumerator 规定实现
    public bool MoveNext()
    {
        index++;
        return index < list.Length;
    }
	
	//IEnumerator 规定实现
    public void Reset()
    {
        index = -1;
    }
}

//yield语法糖版本
class CustomList2<T> : IEnumerable
{
    private T[] list;
    public CustomList2(params T[] list) 
    { 
        this.list = list;
    }
    public IEnumerator GetEnumerator()
    {
        for (int i = 0; i < list.Length; i++)
        {
            //yield关键字配合迭代器理解
            //可以理解为暂时返回保留当前的状态
			//即,yield以后直接跳出,挂起迭代器当前状态
			//当迭代器继续执行的时候又直接进入到这个for代码块当中,执行到yield又继续跳出
			//一个yield语句省下了2个函数的撰写
            yield return list[i];
        }
    }
}

//类声明结束--------------------------------------------------------------
class Program
{
    static void Main(string[] args)
    {
		//标准版本
        CustomList list = new CustomList();
		
		//标准版本--foreach版本
        foreach (int item in list)
        {
            Console.WriteLine(item);
        }
		
		//标准版本--while版本
		var e = list.GetEnumrator();
		while(e.MoveNext())
		{
			Console.WriteLine(e.Current());
		}
        Console.ReadKey();
		
		//语法糖版本
		int[] arr = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
		
		//语法糖版本 foreach的循环
		CustomList<int> custom = new Program.CustomList<int>(arr);
		foreach(var itor in custom)
		{
			Console.WriteLine(itor);
		}
    }
}

Part2 原理解析

知识补丁:关于Yield语法糖的重要补充

在正式介绍原理之前,我们需要重新认识一下yield的实现原理

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");
            }
 
        }
}

输出如下:

Unity如何当协程结束执行另一个协程 unity协程的工作原理_学习

以上的例子可以看出enumerableFuc方法被截成了3个部分,通过MoveNext()返回的bool来判断迭代器是否走完,而yield return的值则通过Current属性返回。整个过程可以理解为当我们调用

IEnumerator enumerator = b.enumerableFuc();

这段代码时,enumerableFuc方法会根据yield关键字将整个方法**拆分成各个代码块,而yield关键字是一个语法糖,背后其实生成了一个新的枚举器类。**枚举器的 MoveNext 方法中就有各个代码块的实现,Current 则存有yield return的对象,然后再封装成IEnumerator返回给enumerator 。yield break语法块会直接返回一个MoveNext 返回值为false的枚举器类,从而终止迭代器运行。

原理解释

铺垫了这么久,终于到原理解释了
首先,我们知道协程有以下的三种调用方式

// case 1
IEnumerator Coroutine1()
{
    //do something xxx		//假如是第N帧执行该语句
    yield return 1;         //等一帧
    //do something xxx  	//则第N+1帧执行该语句
}

// case 2
IEnumerator Coroutine2()
{
    //do something xxx		//假如是第N秒执行该语句
    yield return new WaitForSeconds(2f);    //等两秒		
    //do something xxx  	//则第N+2秒执行该语句
}

// case 3
IEnumerator Coroutine3()
{
    //do something xxx
    yield return StartCoroutine(Coroutine1());  //等协程Coroutine1执行完			
    //do something xxx 	
}

我们通过之前对Yield语法糖和C#迭代器的实现的理解,不难猜到

协程底层实现应该是Unity通过某种算法,将迭代器的MoveNext和Current这两个重要操作的语法糖yield版本根据Unity的框架按照给定的条件,如分帧,延时等待,协程嵌套去分割到程序的不同时机当中去执行

先定义一个协程

class Test
{
     static IEnumerator GetCounter()
     {
           for(int count = 0; count < 10; count++)
           {
                yiled return count;
           }
      }
}

猜了一点东西以后,尝试反编译如下(感谢原作者的反编译素材),发现协程其本质其实是通过IEnumerator迭代器实现的一种状态机,故其本质还是单线程的,一旦协程卡住整个线程也会卡住。

internal class Test  {  
    // GetCounter获得结果就是返回一个实例对象 
    private static IEnumerator GetCounter()  
    {  
        return new <GetCounter>d__0(0);  
    }  
  
    // Nested type automatically created by the compiler to implement the iterator  
    [CompilerGenerated]  
    private sealed class <GetCounter>d__0 : IEnumerator<object>, IEnumerator, IDisposable  
    {  
        // Fields: there'll always be a "state" and "current", but the "count"  
        // comes from the local variable in our iterator block.  
        private int <>1__state;  
        private object <>2__current;  
        public int <count>5__1;  
      
        [DebuggerHidden]  
        public <GetCounter>d__0(int <>1__state)  
        {  
           //初始状态设置
            this.<>1__state = <>1__state;  
        }  
  
        // Almost all of the real work happens here  
        //类似于一个状态机,通过这个状态的切换,可以将整个迭代器执行过程中的堆栈等环境信息共享和保存
        private bool MoveNext()  
        {  
            switch (this.<>1__state)  
            {  
                case 0:  
                    this.<>1__state = -1;  
                    this.<count>5__1 = 0;  
                    while (this.<count>5__1 < 10)        //这里针对循环处理  
                    {  
                        this.<>2__current = this.<count>5__1;  
                        this.<>1__state = 1;  
                        return true;  
                    Label_004B:  
                        this.<>1__state = -1;  
                        this.<count>5__1++;  
                    }  
                    break;  
  
                case 1:  
                    goto Label_004B;  
            }  
            return false;  
        }  
  
        [DebuggerHidden]  
        void IEnumerator.Reset()  
        {  
            throw new NotSupportedException();  
        }  
  
        void IDisposable.Dispose()  
        {  
        }  
  
        object IEnumerator<object>.Current  
        {  
            [DebuggerHidden]  
            get  
            {  
                return this.<>2__current;  
            }  
        }  
  
        object IEnumerator.Current  
        {  
            [DebuggerHidden]  
            get  
            {  
                return this.<>2__current;  
            }  
        }  
    }  
}

【以下解释为重点,面试就回答这里就行了】:代码比较直观,相关的注释也写了一点,所以我们在执行开启一个协程的时候,其本质就是返回一个迭代器的实例,然后在主线程中,每次update的时候,都会更新这个实例,判断其是否执行MoveNext的操作,如果可以执行(比如文件下载完成),则执行一次MoveNext,将下一个对象赋值给Current(MoveNext需要返回为true, 如果为false表明迭代执行完成了)。

通过这儿,可以得到一个结论,协程并不是异步的,其本质还是在Unity的主线程中执行,每次update的时候都会触发是否执行MoveNext。

关于返回值的补充

Unity实际上是可以对所有的继承自YieldInstruction的类去使用协程的,也就是说Yied关键字对所有的YieldInstruction继承的类都是生效的。

支持的返回值如下:

yield return null;//下一帧执行后续代码
yield return 数字;//下一帧执行后续代码
yield break;//结束迭代器
 
yield return new AsyncOperation();//Unity用于进行基于协程的异步操作的基类
yield return IEnumerator;//协程的嵌套
yield return new Coroutine();//用于协程的嵌套和监听Coroutine类是StartCoroutine返回的协程句柄
yield return new WWW();//WWW继承CustomYieldInstruction,CustomYieldInstruction可以用于实现自定义协程响应类
yield return new WaitForEndOfFrame();//在这帧结束,在 Unity 渲染每一个摄像机和 GUI 之后,在屏幕上显示下一帧之前执行后续代码。
yield return new WaitForFixedUpdate();//下一帧FixedUpdate开始时执行后续代码
yield return new WaitForSeconds(等待时间/秒);//延时设置的等待时间之后一帧的Update执行完成后运行,收Time.timeScale影响
yield return new WaitForSecondsRealtime(等待时间/秒);//延时设置的等待时间之后一帧的Update执行完成后运行

一点课后总结作业:丐中丐级协程

纯C#,原生.Net下模拟Unity的协程的实现

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 CoroutinesManager
{
        public List<IEnumerator> coroutines = new List<IEnumerator>();
        //模仿协程开始,将协程加入到协程的执行列表中
        public void StartCoroutine(IEnumerator coroutine)
        {
            coroutines.Add(coroutine);
        }
}

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协程的工作原理_c#_02