Unity中的协程(Coroutine,协同程序)

一、什么是协程

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下。

在一个协程开始后,同样会有结束协程的方法StopCoroutineStopAllCoroutines两种方式,需要注意的是,两者的使用需要遵循一定的规则。在此之前,先介绍一下关于StopCoroutine重载:

StopCoroutine(string methodName):通过方法名(字符串)来进行
StopCoroutine(IEnumerator routine):通过方法形式来调用
StopCoroutine(Coroutine routine):通过指定的协程来关闭

前两种结束协程方法的使用上,如果我们是使用StartCoroutine(string methodName)来开启一个协程的,那么结束协程就只能使用StopCoroutine(string methodName)StopCoroutine(Coroutine routine)来结束协程。

设置gameobjectactivefalse时可以终止协同程序,但是再次设置为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博客

—— 完 ——
相关推荐
评论

立 为 非 似

中 谁 昨 此

宵 风 夜 星

。 露 , 辰

文章点击榜

细 无 轻 自

如 边 似 在

愁 丝 梦 飞

。 雨 , 花