协程的原理自己基本看一遍忘一遍,因此想自己整理一些资料好让资料能留一些在脑子里面
协程
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)
语句,流程如下:
- 先执行 in 后面的 colletion.GetEnumerator 获取迭代器
- 执行获取到的迭代器的 MoveNext 方法
- 执行到的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");
}
}
}
输出如下:
以上的例子可以看出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);
}
}
}
}