Unity的协程详解

Posted vinkey_st

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity的协程详解相关的知识,希望对你有一定的参考价值。

一、协程的定义

协程,即为协同程序. Unity中的协程由协程函数和协程调度器两部分构成.协程函数使用的是C#的迭代器, 协程调度器则利用了MonoBehaviour中的生命周期函数来实现. 协程函数实现了分步, 协程调度器实现了分时. 

注:因为协程分时分步执行的特性,当多个协程的耗时操作挤在同一时间执行也会造成卡顿。

二、协程的用法

using System.Collection;
using UnityEngine;

// 定义一个协程函数,返回一个迭代器接口
IEnumerator CoroutineFunc()

    Debug.Log("第一次进入");
    yield return null;
    Debug.Log("第二次进入");
    yield return null;


// 在继承自MonoBehaviour的类中调用此协程函数
void Start()

    // 获取迭代器接口
    IEnumerator enumerator = CoroutineFunc();
    // 返回的Coroutine对象保存起来可用于停止协程
    Coroutine coroutine = StartCoroutine(enumerator);
    // 相当于在外部 yield break;
    StopCoroutine(coroutine);

三、Unity规定的协程返回值的含义

含义代码
下一帧再执行后续代码yield return null;  yield retun x(x代表任意数字)
结束该协程yield break;
等待固定时间执行后续代码

yield return new WaitForSeconds(0.3f);

yield return new WaitForSecondsRealtime(0.3f); //不受timescale影响

函数执行完毕后执行后续代码yield return FunctionName();
异步执行完毕后执行后续代码yield return AsyncOperation;
协程执行完毕后执行后续代码yield return Coroutine;
帧渲染完成后执行后续代码yield return new WaitForEndOfFrame();
物理帧更新后执行后续代码yield return new WaitForFixedUpdate();
参数为true时执行后续代码yield return new WaitUntil(arg);
参数为false时执行后续代码yield return new WaitWhile(arg);

注: 为了优化性能,yield return 后面需要new的返回值应该预先创建, 而不是在协程函数中反复创建.

各个 yield return 在生命周期的位置

四、协程函数与普通函数的区别

操作协程函数普通函数
返回值可分步返回多次只能返回一次
获取返回值的方式调用后执行MoveNext(),通过Current属性获取当前返回值;调用函数;
返回顺序  根据实际情况交错返回根据调用顺序返回

注:执行协程函数返回的是一个迭代器接口而并非得到结果

五、协程与多线程的联系与区别

区别:

协程多线程
切换时机自定CPU时间片为单位的系统调度
CPU核心与主线程在同一核心根据操作系统调度不同
对主线程的影响卡顿会影响主线程卡死都不会影响主线程
线程同步问题不存在线程同步问题需要注意线程同步问题
线程开销不存在线程开销存在线程创建、销毁、切换的开销
书写方式与普通函数一致回调函数

联系:

协程与多线程都是异步操作,都是为了提高CPU的利用率存在的。

六、Unity协程的原理

using System;
using System.Collection;
using System.Collection.Gernic;
using UnityEngine;

// 感谢唐老师的指导

public class YieldInstruction

    public IEnumerator ie;
    public float executeTime;




public class CoroutineMgr : MonoBehaviour

    private List<YieldInstruction> list = new List<YieldInstruction>();

    public void StartCoroutine(IEnumerator ie)
    
        ie.MoveNext();
        if((ie.Current is null) || (ie.Current is int))
        
            list.Add(new YieldInstruction ie=ie,executeTime=0; );
        
        else if(ie.Current is WaitForSeconds)
        
            list.Add(new YieldInstruction 
                ie=ie,
                executeTime=Time.time+(ie.Currentas WaitForSeconds).second );
        
        else if (...)
        ...
    



    void Update()
    
        // 倒序遍历方便移除
        for(int i=list.Count-1; i>=0; i--)
        
            if(list[i].executeTime<=Time.time)
            
                if(list[i].ie.MoveNext())
                
                    // 如果是已定义的类型
                    if((ie.Current is null) 
                    || (ie.Current is int)) 
                    || (ie.Current is WaitForSeconds))
                    
                        // 继续指定执行时机
                    
                    else
                    
                        list.RemoveAt(i);
                    
                
                else
                
                    list.RemoveAt(i);
                
            
        
    

七、Unity协程的垃圾的来自何处

在我尚不了解协程的时候, 用协程制作了真炎幸魂的终极技能:八重火垣. 因在协程中循环使用协程, 在我顾忌协程是否有坏处的时候, 发现了这么一句话:

协程的坏处:
协程本质是迭代器,且是基于unity生命周期的,大量开启协程会引起gc

这句话对我小小的心灵造成了巨大的伤害:

"我的杰作刚出生就要宣判死刑了吗!!!!!  不要!!!!!!!   嘤嘤嘤~~~"

即使是现在的我, 依然不太能理解这句话. 一连串的问题从我脑海中冒出来了.

这GC是协程的问题还是大量的问题?

本质是迭代器是坏处? 基于unity声明周期是坏处? 大量开启是协程的问题不是使用者的问题?

同样的东西协程就比普通函数更容易GC吗?

不是只有引用类型会GC吗?

基于Unity生命周期就会GC吗?

垃圾到底在哪里?

垃圾的产生无法控制吗?

这前言不搭后语的一句话真是让人误会, 而且我还发现多处地方都引用了这句话, 却完全没人把这句话说明白.


疑惑存在, 实验开始.

1.当前版本的Unity使用协程是否会产生GC

实验代码:

public class TestMono : MonoBehaviour

    public int times = 100000;
    public bool useStr = true;

    void Start()
    
        for (int i = 0; i < times; i++)
        
            if (useStr)
                StartCoroutine("CoroutineFunc");
            else
                StartCoroutine(CoroutineFunc());
        
    

    IEnumerator CoroutineFunc()
    
        yield return null;
    

实验数据取 Unity Profiler 5秒内 GC Used Memory 最高点

项目/实验次数123456
不启动协程13.1M11.8M12.2M12.1M12.4M12.4M
传入字符串24.0M28.0M27.7M28.1M28.4M28.2M

传入IEnumerator

28.6M28.2M28.7M28.5M28.6M28.5M

小结: 使用协程的确会产生垃圾, 且两种方式产生的内存不相上下.

2. 协程的垃圾产生在迭代器还是调度器?

由于调度器调度不存在的函数会报错, 所以只能从迭代器入手进行测试.

实验代码:

public class TestMono : MonoBehaviour

    public int times = 100000;
    public bool useStr = true;


    // Start is called before the first frame update
    void Start()
    
        for (int i = 0; i < times; i++)
        
            if (useStr)
                enumerator = "CoroutineFunc";
            else
                CoroutineFunc();
        
    

    IEnumerator CoroutineFunc()
    
        yield return null;
    

项目/实验次数123456
不启动协程11.9M12.0M12.0M12.2M11.7M12.0M
传入字符串12.7M11.8M12.2M12.2M12.2M12.4M

传入IEnumerator

12.4M12.5M12.4M12.6M12.5M12.6M

小结: 通过对比一阶段的数据基本可以确定产生垃圾的主要位置在调度器. 但同时能发现迭代器对象也会造成微量的垃圾.

3. 是否只是单纯的使用调度器就会产生大量GC?

实验代码:

public class TestMono : MonoBehaviour

    public int times = 100000;
    private IEnumerator enumerator;


    // Start is called before the first frame update
    void Start()
    
        enumerator = CoroutineFunc();
        for (int i = 0; i < times; i++)
        
            StartCoroutine(enumerator);
        
    

    IEnumerator CoroutineFunc()
    
        yield return null;
    

项目/实验次数123456
不启动协程17.5M15.9M16.5M16.1M16.5M16.6M

传入IEnumerator

17.4M17.0M17.5M16.9M17.4M17.5M

小结: 只是单纯的调用StartCoroutine并不会产生大量垃圾, 那么第一轮的垃圾应该是一个可以遍历的迭代器和调度器共同作用产生的. 可惜IEnumerator禁止使用Reset, 不然可以测试的更全面.

结论

使用协程时会产生垃圾, 且此垃圾不可控制.

原因: 协程函数的调用会实例化一个接口对象, 而接口是引用类型.

         StartCoroutine对协程的调用会不可避免的产生较多垃圾.

         迭代器对象无法Reset, 想要重复执行相同逻辑只能再次创建迭代器对象.

Unity中的协程用法以及注意事项

  前沿:这章节,将简单的总结一下如何开启协程,关闭协程,以及使用协程的注意事项。

 

  一、如何开启协程:

private void Start()
    {
        m_SpherePrefab = Resources.Load<GameObject>("Test/Sphere_00");
        Debug.Log("m_SpherePrefab = " + m_SpherePrefab);

        #region 协程的学习及使用

        StartCoroutine(Test_00());
        StartCoroutine("Test_01");
        StartCoroutine(Test_02(5, 9));

        #endregion
    }

    private IEnumerator Test_00()
    {
        Debug.Log("协程 Test_00 准备执行");
        yield return new WaitForSeconds(3.0f);
        Debug.Log("协程 Test_00 执行完毕");
    }

    private IEnumerator Test_01()
    {
        Debug.Log("协程 Test_01 准备执行");
        yield return new WaitForSeconds(5.0f);
        Debug.Log("协程 Test_01 执行完毕");
    }

    private IEnumerator Test_02(int a, int b)
    {
        Debug.Log("协程 Test_02 准备执行");
        yield return new WaitForSeconds(8.0f);
        Debug.Log("协程 Test_02:" + a + " " + b);
        Debug.Log("协程 Test_02 执行完毕");
    }

    这里,例举了开启协程的3种方法,以及协程传递数据的使用方法。

 

  二、如何停止协程

private void Update()
    {
        if (m_CurrentCount < m_MaxCount)
        {
            if (Time.time > m_NextCloneTime)
            {
                //克隆
                Clone();
            }
        }

        if (Input.GetKeyUp(KeyCode.T))
        {
            //通过 StopCoroutine 停止协程的时候,需要用方法名来停止,那么在开启协程的时候,也需要使用方法名来开启
            //错误的停止方式
            //StopCoroutine(Test_00());
        }

        if (Input.GetKeyUp(KeyCode.Y))
        {
            //通过 StopCoroutine 停止协程的时候,需要用方法名来停止,那么在开启协程的时候,也需要使用方法名来开启
            //正确的停止方式
            StopCoroutine("Test_01");
        }

        if (Input.GetKeyUp(KeyCode.U))
        {
            StopAllCoroutines();
        }
    }

    这里,指示了停止协程的方法。

 

  三、注意事项

    1.若是停止某个指定的协程(StopCoroutine),则填写的参数应该是方法名,并且开启这个协程的时候,填写的参数应当也是方法名。

    2.在使用使用StopAllCoroutines停止协程时,应当注意到,不是停止所有的协程,而是停止当前脚本下的所有协程。

以上是关于Unity的协程详解的主要内容,如果未能解决你的问题,请参考以下文章

在foreach循环Unity3D C#中的协程

Unity优化如何实现Unity编辑器中的协程

unity里头的协程

实现一个简单的Unity网络同步引擎——netgo

unity 中的协程

Unity中的协程用法以及注意事项