对Unity协程(Coroutine)的理解

【前置知识】

对进程和线程的理解

【为什么要有协程】

游戏中逻辑更新和画面更新的时间点有确定性,必须按照帧序列严格保持同步,否则就会出现游戏中对象不同步的现象。虽然多线程也能实现对象同步等效果,但是对一个大型游戏而言,游戏中的对象非常多,用多线程来实现很容易出Bug,而且往往很难找到Bug在哪,这无疑会加大开发的难度。

为了开发便利,统一生命周期管理,避免多线程锁的问题,Unity采用单线程逻辑。

为此,UnityEngine对其API和Componet等做出了限制,不能在主线程之外的线程中使用它们,这就确保了它们始终是在单线程中运行。

但是,对于一些耗时的操作(即需要较长时间才能执行完的代码块),我们不能一直等待其执行完毕再执行接下来的操作,否则游戏会出现卡顿等现象。例如,在游戏中等待1s再去执行某个操作时,如果我们让主线程等待1s,那么游戏画面就会停顿1s,这是不行的。为了解决这类问题,就需要使用协程——在主线程运行的同时开启另一段逻辑处理,来协同当前程序的执行

显然,协程的出现是为了解决单线程的问题,如同线程的出现是为了解决进程的问题一样。我们可以想象,如果程序越来越复杂,进程之上还会有xx程,协程之下还会有xx程。

【协程的特点及执行过程】

【测试例子】

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Coroutine : MonoBehaviour
{
    private bool isStart = true;
    void Start()
    {
        Debug.Log("开始执行Start" + Time.frameCount);
        StartCoroutine(Test());
        Debug.Log("结束执行Start" + Time.frameCount);
    }

    void Update()
    {
        if (isStart)
        {
            Debug.Log("开始执行Update"+ Time.frameCount);
            isStart = false;
            StartCoroutine(Test());
            Debug.Log("结束执行Update" + Time.frameCount); 
        }
       
    }


    IEnumerator Test()//冒泡排序
    {
        int[] A = new int[10];
        System. Random ra = new System.Random();
        for (int i = 0; i < 10; i++)
        {
            A[i] = ra.Next(200);
        }
        bubblesort(A);
        foreach (int a in A)
        {
            Debug.Log(a);
        }
        Debug.Log("开始执行协程" + Time.frameCount);
       yield return null;
       Debug.Log("结束执行协程"+Time.frameCount);
    }

    public void bubblesort(int[] A)
    {
        int n = A.Length;
        for (int i = 0; i < A.Length - 1; i++)
        {
            for (int j = 0; j < A.Length - 1; j++)
            {
                if (A[j] > A[j + 1])
                {
                    int temp = A[j];
                    A[j] = A[j + 1];
                    A[j + 1] = temp;
                }
            }
        }

    }

}

【测试结果】

【结论】

显而易见,我们在调用协程时(即使用StartCoroutine()函数时),程序会先继续执行协程里的代码,而不是立即返回,继续执行下一行代码,遇到yield 时,协程停止执行,再返回执行下一行代码。

那么yield后的代码在什么时候执行呢?

在Unity官方文档中的生命周期中可以看到yield后的代码在Update执行完之后再执行,即在所有脚本中的Update执行完后,如果yield后面还有代码就执行其后的代码,而且,yield后面的代码要在下一帧中执行

所以,yield的作用是中断协程的执行。在执行协程的过程中,如果遇到yield,则表示当前帧执行到这里停止,不再继续执行下一行代码,所以这个协程没有执行完毕,在下一帧中会再次执行这个协程,并且从yield开始执行。

如果协程要在下一帧能够从yield开始执行,那么协程就有自己的堆栈,自己的局部变量,有自己的指令指针。总而言之,要保存在上一帧中执行协程时所得的结果。

yield后面可以跟一个表达式,函数等。当下一帧从yield开始执行时,会判断yield后面的条件是否满足。如果满足,就继续执行下一行代码;如果不满足,在当前帧中,协程执行到这里就停止,下一帧继续判断是否满足条件,直到满足条件才继续执行下一行。

【yield返回值及含义】(在C#中是yield return)

yield return null; 下一帧再执行后续代码
yield return 0; 下一帧再执行后续代码
yield return 6;(任意数字) 下一帧再执行后续代码
yield break; 直接结束该协程的后续操作
yield return asyncOperation;等异步操作结束后再执行后续代码
yield return StartCoroution(/*某个协程*/);等待某个协程执行完毕后再执行后续代码
yield return WWW();等待WWW操作完成后再执行后续代码
yield return new WaitForEndOfFrame();等待帧结束,等待直到所有的摄像机和GUI被渲染完成后,在该帧显示在屏幕之前执行
yield return new WaitForSeconds(0.3f);等待0.3秒,一段指定的时间延迟之后继续执行,在所有的Update函数完成调用的那一帧之后(这里的时间会受到Time.timeScale的影响);
yield return new WaitForSecondsRealtime(0.3f);等待0.3秒,一段指定的时间延迟之后继续执行,在所有的Update函数完成调用的那一帧之后(这里的时间不受到Time.timeScale的影响);
yield return WaitForFixedUpdate();等待下一次FixedUpdate开始时再执行后续代码
yield return new WaitUntil()将协同执行直到 当输入的参数(或者委托)为true的时候....如:yield return new WaitUntil(() => frame >= 10);
yield return new WaitWhile()将协同执行直到 当输入的参数(或者委托)为false的时候.... 如:yield return new WaitWhile(() => frame < 10);
yield return X>5;等待变量x大于5时再执行后续代码
yield return transform.position.y<0;等待物体的高度小于0时再执行后续代码

如果在当前帧就可以执行完协程中的最后一行代码,那么就表示这个协程已经执行完毕。在下一帧中,会继续执行所有的Update,但不会再执行这个在上一帧中已经执行完毕的协程。(Start和Update中的协程虽然都是调用Test,但它们是两个协程,而不是一个。)

所以,协程实际上是运行在Unity主线程中,有多个协程时,这些协程依次使用CPU,其他协程处于休眠状态(即CPU每次只响应一个协程的计算请求,只有完成计算请求后,才会响应下一个协程的计算请求),这就类同单核CPU响应多个线程请求的情况,也类同多个脚本中多个Update依次执行。

【线程和协程的区别】

  • 在多核CPU下,同一时间可以同时执行多个线程,但只能执行一个协程
  • 开启一个线程比开启一个协程的开销大
  • 线程适合多个关联性不大的任务同时处理,协程适合将一个任务分时间片处理
  • 线程是操作系统级别的概念,开发者无法预估线程在何时被调度执行;协程是编译器级别的概念,协程的调度需要开发者自己去实现
  • 线程不能使用UnityEngine的API,协程可以
  • ······各类区别很多,弄明白执行过程后,意思都差不多

【协程的使用】

【注意事项】

  • 必须在MonoBehaviour或继承于MonoBehaviour的类中(类似Update)
  • 协程的启动和终止只是对该 MonoBehaviour而言(类似Update)

【启动协程】

  • StartCoroutine(IEnumerator routine)
  • StartCoroutine(string methodName)或StartCoroutine(string routine,object value) (比上一种方法性能开销要更高,而且最多只能传递 一个参数)

【终止协程】

  • StopCoroutine(string methondname)只能终止通过字符串名字启动的协程
  • StopCoroutine(Coroutine coroutine)
  • StopAllCoroutines()终止所有协程
  • 在协程内部终止:yield break;
  • 把物体的active属性设置为false(类似Update,该方式容易引发bug)

【案例】

每隔1秒输出一个信息,输出10次

【代码】

   IEnumerator Test2()
    {
        for (int i = 0; i < 10; i++)
        {
            Debug.Log("永恒之星");
            yield return new WaitForSeconds(1.0f);
        }
    }

【输出】

 

【参考】

[1]https://blog.csdn.net/qiaoquan3/article/details/51276485

[2]https://docs.unity3d.com/Manual

[3]https://blog.csdn.net/qq_28180261/article/details/64500720

[4]https://blog.csdn.net/hany3000/article/details/10382221

[5]https://blog.csdn.net/huang9012/article/details/38492937

[6]https://blog.csdn.net/beihuanlihe130/article/details/76098844

[7]https://blog.csdn.net/u011484013/article/details/51136780

[8]https://zhuanlan.zhihu.com/p/55420579

[9]https://blog.csdn.net/qq_30585525/article/details/90769410

[10]https://www.iteye.com/blog/dsqiu-2029701

  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值