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");elseStartCoroutine(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 updatevoid Start(){for (int i = 0; i < times; i++){if (useStr)enumerator = "CoroutineFunc";elseCoroutineFunc();}}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 updatevoid 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的协程详解相关推荐
- python协程详解
目录 python协程详解 一.什么是协程 二.了解协程的过程 1.yield工作原理 2.预激协程的装饰器 3.终止协程和异常处理 4.让协程返回值 5.yield from的使用 6.yield ...
- python协程详解_python协程详解
原博文 2019-10-25 10:07 − # python协程详解 ![python协程详解](https://pic2.zhimg.com/50/v2-9f3e2152b616e89fbad86 ...
- Unity C#笔记 协程详解(转)
目录 什么是协程 多线程 协程 协程的使用场景 协程使用示例 Invoke的缺陷 协程语法 开启协程 终止协程 挂起 协程的执行原理 什么是协程 在Unity中,协程(Coroutines)的形式是我 ...
- python协程详解_对Python协程之异步同步的区别详解
一下代码通过协程.多线程.多进程的方式,运行代码展示异步与同步的区别. import gevent import threading import multiprocessing # 这里展示同步和异 ...
- Python进程、线程、协程详解
进程与线程的历史 我们都知道计算机是由硬件和软件组成的.硬件中的CPU是计算机的核心,它承担计算机的所有任务. 操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配.任务的调度. ...
- coroutine协程详解
前两天阿里巴巴开源了coobjc,没几天就已经2千多star了,我也看了看源码,主要关注的是协程的实现,周末折腾了两整天参照Go的前身libtask和风神的coroutine实现了一部分,也看了一些文 ...
- 【转载】Python线程、进程和协程详解
从操作系统角度 操作系统处理任务,调度单位是进程和线程. 进程:表示一个程序的执行活动(打开程序.读写程序数据.关闭程序) 线程:执行某个程序时,该进程调度的最小执行单位(执行功能1,执行功能2) 一 ...
- python多核cpu_Python中的多核CPU共享数据之协程详解
一 : 科普一分钟 尽管进程间是独立存在的,不能相互访问彼此的数据,但是在python中却存在进程间的通信方法,来帮助我们可以利用多核CPU也能共享数据. 对于多线程其实也是存在一些缺点的,不是任何场 ...
- python协程详解_彻底搞懂python协程-第一篇(关键词1-4)
任何复杂的概念或系统都不是凭空出现的,我们完全可以找到它的演化历程,寻根究底终会发现,其都是在一系列并不那么复杂的简单组件上发展演化而来! by 落花僧 本文通过一系列关键概念,逐步递进理解协程. 0 ...
最新文章
- ​利用卷积神经网络学习脑电地形图表示进行分类
- hdu 5124(线段树区间更新+lazy思想)
- 牛客题霸 [斐波那契数列] C++题解/答
- pythontk界面显示函数中的变量值_简单易学,西门子触摸屏3种修改变量值的方法!博图Wincc V14组态...
- 博客园修改TinyMCE编辑器为Markdown编辑器的方法
- Sybase迁移Oracle字符集问题,Sybase数据库迁移数据到Oracle(未改进)
- 杨柳青镇cad_CAD制图岗位职责|CAD制图工作内容 - 职业圈
- 盘点机PDA搭配蓝牙便携打印机,条码标签打印,超市仓库条码管理,条码标签纸
- 回归分析(数据拟合---MATLAB和1stopt软件)
- 小度加速破圈,智能音箱告别肉搏战
- python多进程传递参数_Python进程,多进程,获取进程id,给子进程传递参数操作示例...
- python中turtle画小草_python 笔记 之带参数的装饰器
- 1896-2021历届奥运会奖牌榜动态排序(Matplotlib图表动画)
- 科研实习 | 北京大学万小军老师课题组招收NLP方向实习生和访问学生
- AM4379 EDMA相关总结
- 计算机公共基础知识(N-S图,DFD图,PAD图,程序流程图,E-R图)
- 感性认识:计算机基本工作原理
- 浅谈神经网络之链式法则与反向传播算法
- base64加密--excel--pdf--img 上传
- MCNP学习笔记之命令行与接续运行