unity 同一时间 能有几个协程
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了unity 同一时间 能有几个协程相关的知识,希望对你有一定的参考价值。
考虑到代码有先后顺序的前提,同一时间应该只有一个协程在工作,但从执行效果来看,因为协程都是在差帧过程中执行的,所以基本可以理解成在同一时间段内,只要系统资源足够支持,可以有无限多的协程并行运行,每个协程之间根据启动点不同,有先后n个执行周期的时间差。纯属个人理解,希望能帮到你 参考技术A yiled break就直接退出方法了。其它的yiled是挂起等待一定的时间后再执行。没有不执行的说法,如果不执行yield那就不是协程,或者该方法你就没有使用协程调用它,还有一种特殊的情况是当前脚本或者脚本所在物体不活动了,导致该物体上的协程全部退出。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的返回值应该预先创建, 而不是在协程函数中反复创建.
四、协程函数与普通函数的区别
操作 | 协程函数 | 普通函数 |
---|---|---|
返回值 | 可分步返回多次 | 只能返回一次 |
获取返回值的方式 | 调用后执行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 最高点
项目/实验次数 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
不启动协程 | 13.1M | 11.8M | 12.2M | 12.1M | 12.4M | 12.4M |
传入字符串 | 24.0M | 28.0M | 27.7M | 28.1M | 28.4M | 28.2M |
传入IEnumerator | 28.6M | 28.2M | 28.7M | 28.5M | 28.6M | 28.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;
项目/实验次数 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
不启动协程 | 11.9M | 12.0M | 12.0M | 12.2M | 11.7M | 12.0M |
传入字符串 | 12.7M | 11.8M | 12.2M | 12.2M | 12.2M | 12.4M |
传入IEnumerator | 12.4M | 12.5M | 12.4M | 12.6M | 12.5M | 12.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;
项目/实验次数 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
不启动协程 | 17.5M | 15.9M | 16.5M | 16.1M | 16.5M | 16.6M |
传入IEnumerator | 17.4M | 17.0M | 17.5M | 16.9M | 17.4M | 17.5M |
小结: 只是单纯的调用StartCoroutine并不会产生大量垃圾, 那么第一轮的垃圾应该是一个可以遍历的迭代器和调度器共同作用产生的. 可惜IEnumerator禁止使用Reset, 不然可以测试的更全面.
结论
使用协程时会产生垃圾, 且此垃圾不可控制.
原因: 协程函数的调用会实例化一个接口对象, 而接口是引用类型.
StartCoroutine对协程的调用会不可避免的产生较多垃圾.
迭代器对象无法Reset, 想要重复执行相同逻辑只能再次创建迭代器对象.
以上是关于unity 同一时间 能有几个协程的主要内容,如果未能解决你的问题,请参考以下文章
Kotlin 协程协程上下文 ( 协程上下文构成要素 | 指定协程上下文元素组合 | 协程上下文元素的继承关系 | 协程上下文元素的几种指定形式 | 默认 | 继承 | 自定义指定 )
Kotlin 协程协程上下文 ( 协程上下文构成要素 | 指定协程上下文元素组合 | 协程上下文元素的继承关系 | 协程上下文元素的几种指定形式 | 默认 | 继承 | 自定义指定 )
Kotlin 协程协程中的多路复用技术 ① ( 多路复用技术 | await 协程多路复用 | Channel 通道多路复用 )
Kotlin 协程协程取消 ① ( 协程作用域取消 | 协程作用域子协程取消 | 通过抛出异常取消协程 | Job#cancel 函数 | 自定义异常取消协程 )