一、什么是协程
A coroutine is like a function that has the ability to pause execution and return control to Unity but then to continue where it left off on the following frame.
协程就像一个函数,能够暂停执行并将控制权返还给 Unity,然后在下一帧继续执行。
在 C# 中,声明协程的方式如下:
IEnumerator Fade()
{
for (float ft = 1f; ft >= 0; ft -= 0.1f)
{
Color c = renderer.material.color;
c.a = ft;
renderer.material.color = c;
yield return null;
}
}
Unity中的协程是一种返回值为IEnumerator
的特殊函数,它可以主动的请求暂停自身并提交一个唤醒条件(yield return 语句),yield return null 行是暂停执行并随后在下一帧恢复的点。Unity会在唤醒条件满足的时候去重新唤醒协程,所以协程还是运行在主线程上。
一个协同程序在执行过程中,可以在任意位置使用yield语句。yield的返回值控制何时恢复协同程序向下执行。
二、协程的使用
要将协程设置为运行状态,必须使用 StartCoroutine 函数:
public Coroutine StartCoroutine(IEnumerator routine);
MonoBehaviour.StartCoroutine()
方法可以开启一个协程,这个协程会挂在该MonoBehaviour下。
在一个协程开始后,同样会有结束协程的方法StopCoroutine
与StopAllCoroutines
两种方式,需要注意的是,两者的使用需要遵循一定的规则。在此之前,先介绍一下关于StopCoroutine重载:
StopCoroutine(string methodName):通过方法名(字符串)来进行
StopCoroutine(IEnumerator routine):通过方法形式来调用
StopCoroutine(Coroutine routine):通过指定的协程来关闭
前两种结束协程方法的使用上,如果我们是使用StartCoroutine(string methodName)
来开启一个协程的,那么结束协程就只能使用StopCoroutine(string methodName)
和StopCoroutine(Coroutine routine)
来结束协程。
设置gameobject
的active
为false
时可以终止协同程序,但是再次设置为true后协程不会再启动。
三、协程的应用场景
- 异步加载资源
- 将一个复杂程序分帧执行
- 定时器
四、协程的执行顺序
开始协同程序 -> 执行协同程序 -> 中断协同程序(中断指令)-> 返回上层继续执行->中断指令结束后继续执行协同程序剩下的内容
一个协程收到中断指令(YieldInstruction)后暂停执行,返回上层执行同时等待这个指令达成后继续执行。
指令 | 描述 | 实现 |
WaitForSeconds | 等待指定秒数 | yield return new WaitForSeconds(2); |
WaitForFixedUpdate | 等待一个固定帧 | yield return new WaitForFixedUpdate(); |
WaitForEndOfFrame | 等待帧结束 | yield return new WaitForEndOfFrame(); |
StartCoroutine | 等待一个新协程结束 | yield return StartCoroutine(other coroutine); |
五、协程的特点
- 1、协程在中断指令(YieldInstruction)产生时暂停执行
- 2、协程一暂停执行便立即返回 //中断协程后返回主函数,暂停结束后继续执行协程剩余的函数。
- 3、中断指令完成后从中断指令的下一行继续执行
- 4、同一时刻、一个脚本实例中可以有多个暂停的协程,但只有一个运行着的协程
- 5、函数体全部执行完后,协程结束
- 6、协程可以很好的控制跨越一定帧数后执行的行为
- 7、协程在性能上、相比于一般函数几乎没有更多的开销
- 8、不能再Update或者FixUpdate方法中使用协同程序,否则会报错。
六、协程的优缺点
优点:
- 协程更加轻量,创建成本更小,降低了内存消耗
- 协作式的用户态调度器,减少了 CPU 上下文切换的开销,提高了 CPU 缓存命中率
- 减少同步加锁,整体上提高了性能
- 可以按照同步思维写异步代码,即用同步的逻辑,写由协程调度的回调
缺点:
- 在协程执行中不能有阻塞操作,否则整个线程被阻塞(协程是语言级别的,线程,进程属于操作系统级别)
- 需要特别关注全局变量、对象引用的使用
- 协程可以处理 IO 密集型程序的效率问题,但是处理 CPU 密集型不是它的长处。
七、协程(Coroutine)、进程(Process)和线程(Thread)的关系与区别
Process -> Thread -> Coroutine
协程(Coroutine)是编译器级的,进程(Process)和线程(Thread)是操作系统级的。
进程(Process)和线程(Thread)是os通过调度算法,保存当前的上下文,然后从上次暂停的地方再次开始计算,重新开始的地方不可预期,每次CPU计算的指令数量和代码跑过的CPU时间是相关的,跑到os分配的cpu时间到达后就会被os强制挂起,开发者无法精确的控制它们。
协程(Coroutine)是一种轻量级的用户态线程,实现的是非抢占式的调度,即由当前协程切换到其他协程由当前协程来控制。
目前的协程框架一般都是设计成 1:N 模式。所谓 1:N 就是一个线程作为一个容器里面放置多个协程。那么谁来适时的切换这些协程?答案是有协程自己主动让出 CPU,也就是每个协程池里面有一个调度器,这个调度器是被动调度的。而且当一个协程发现自己执行不下去了(比如异步等待网络的数据回来,但是当前还没有数据到),这个时候就可以由这个协程通知调度器,这个时候执行到调度器的代码,调度器根据事先设计好的调度算法找到当前最需要 CPU 的协程。切换这个协程的 CPU 上下文把 CPU 的运行权交个这个协程,直到这个协程出现执行不下去需要等等的情况,或者它调用主动让出 CPU 的 API 之类,触发下一次调度。
八、协程的底层原理
协程分为两部分,协程与协程调度器:协程仅仅是一个能够中间暂停返回的函数,而协程调度是在MonoBehaviour的生命周期中实现的。 准确的说,Unity只实现了协程调度部分,而协程本身其实就是用了C#原生的”迭代器方法“。
1、协程(C#的迭代器函数)
C#中的迭代器方法其实就是一个协程,你可以使用yield来暂停,使用MoveNext()来继续执行。 当一个方法的返回值写成了IEnumerator类型,他就会自动被解析成迭代器方法(后文直接称之为协程),你调用此方法的时候不会真的运行,而是会返回一个迭代器,需要用MoveNext()来真正的运行。看例子:
static void Main(string[] args)
{
IEnumerator it = Test();//仅仅返回一个指向Test的迭代器,不会真的执行。
Console.ReadKey();
it.MoveNext();//执行Test直到遇到第一个yield
System.Console.WriteLine(it.Current);//输出1
Console.ReadKey();
it.MoveNext();//执行Test直到遇到第二个yield
System.Console.WriteLine(it.Current);//输出2
Console.ReadKey();
it.MoveNext();//执行Test直到遇到第三个yield
System.Console.WriteLine(it.Current);//输出test3
Console.ReadKey();
}
static IEnumerator Test()
{
System.Console.WriteLine("第一次执行");
yield return 1;
System.Console.WriteLine("第二次执行");
yield return 2;
System.Console.WriteLine("第三次执行");
yield return "test3";
}
- 执行Test()不会运行函数体,会直接返回一个IEnumerator
- 调用IEnumerator的MoveNext()成员,会执行协程直到遇到第一个yield return或者执行完毕。
- 调用IEnumerator的Current成员,可以获得yield return后面接的返回值,该返回值可以是任何类型的对象。
2、协程调度:MonoBehaviour生命周期中实现
MonoBehaviour生命周期中,会有很多yield
阶段,在这些阶段中,Unity会检查MonoBehaviour中是否挂载了可以被唤醒的协程,如果有则唤醒它。
通过对C#迭代器的了解,我们可以模仿Unity自己实现一个简单的协程调度。这里以YieldWaitForSeconds
为例:
// 伪代码
void YieldWaitForSeconds()
{
//定义一个移除列表,当一个协程执行完毕或者唤醒条件的类型改变时,应该从当前协程列表中移除。
List<WaitForSeconds> removeList = new List<WaitForSeconds>();
foreach(IEnumerator w in m_WaitForSeconds) //遍历所有唤醒条件为WaitForSeconds的协程
{
if(Time.time >= w.beginTime() + w.interval) //检查是否满足了唤醒条件
{
//尝试唤醒协程,如果唤醒失败,则证明协程已经执行完毕
if(it.MoveNext();)
{
//应用新的唤醒条件
if(!(it.Current is WaitForSeconds))
{
removeList.Add(it);
//在这里写一些代码,将it移到其它的协程队列里面去
}
}
else
{
removeList.Add(it);
}
}
}
m_WaitForSeconds.RemoveAll(removeList);
}
九、参考资料
1、Unity基础篇:协程(协同程序)的概括(StartCoroutine 和yield return和StopCoroutine )_烟雨迷离半世殇的博客-CSDN博客
2、【Unity】Unity协程(Coroutine)的原理与应用_我是飞扬的博客-CSDN博客