Unity 协程(Coroutine)到底是什么?

news2024/9/22 10:07:12

参考链接:Unity 协程(Coroutine)原理与用法详解_unity coroutine-CSDN博客

为啥在Unity中一般不考虑多线程

  • 因为在Unity中,只能在主线程中获取物体的组件、方法、对象,如果脱离这些,Unity的很多功能无法实现,那么多线程的存在与否意义就不大了

既然这样,线程与协程有什么区别呢:

  • 对于协程而言,同一时间只能执行一个协程,而线程则是并发的,可以同时有多个线程在运行
  • 两者在内存的使用上是相同的,共享堆,不共享栈

其实对于两者最关键,最简单的区别是微观上线程是并行(对于多核CPU)的,而协程是串行的,如果你不理解没有关系,通过下面的解释你就明白了

关于协程
1,什么是协程

协程,从字面意义上理解就是协助程序的意思,我们在主任务进行的同时,需要一些分支任务配合工作来达到最终的效果

稍微形象的解释一下,想象一下,在进行主任务的过程中我们需要一个对资源消耗极大的操作时候,如果在一帧中实现这样的操作,游戏就会变得十分卡顿,这个时候,我们就可以通过协程,在一定帧内完成该工作的处理,同时不影响主任务的进行

2,协程的原理
首先需要了解协程不是线程,协程依旧是在主线程中进行

然后要知道协程是通过迭代器来实现功能的,通过关键字IEnumerator来定义一个迭代方法,注意使用的是IEnumerator,而不是IEnumerable

两者之间的区别:

  • IEnumerator:是一个实现迭代器功能的接口
  • IEnumerable:是在IEnumerator基础上的一个封装接口,有一个GetEnumerator()方法返回IEnumerator
3、协程的使用

首先通过一个迭代器定义一个返回值为IEnumerator的方法,然后再程序中通过StartCoroutine来开启一个协程即可:

 	//通过迭代器定义一个方法
 	IEnumerator Demo(int i)
    {
        //代码块

        yield return 0; 
		//代码块
       
    }

    //在程序种调用协程
    public void Test()
    {
        //第一种与第二种调用方式,通过方法名与参数调用
        StartCoroutine("Demo", 1);

        //第三种调用方式, 通过调用方法直接调用
        StartCoroutine(Demo(1));
    }

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

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

刚刚我们说到他们的使用是有一定的规则的,那么规则是什么呢,答案是前两种结束协程方法的使用上,如果我们是使用StartCoroutine(string methodName)来开启一个协程的,那么结束协程就只能使用StopCoroutine(string methodName)和StopCoroutine(Coroutine routine)来结束协程,可以在文档中找到这句话:

Unity生命周期:

首先解释一下位于Update与LateUpdate之间这些yield 的含义:

  • yield return null; 暂停协程等待下一帧继续执行
  • yield return 0或其他数字; 暂停协程等待下一帧继续执行
  • yield return new WairForSeconds(时间); 等待规定时间后继续执行
  • yield return StartCoroutine("协程方法名");开启一个协程(嵌套协程)

接下来看几个特殊的yield,他们是用在一些特殊的区域,一般不会有机会去使用,但是对于某些特殊情况的应对会很方便

  • yield return GameObject; 当游戏对象被获取到之后执行
  • yield return new WaitForFixedUpdate():等到下一个固定帧数更新
  • yield return new WaitForEndOfFrame():等到所有相机画面被渲染完毕后更新
  • yield break; 跳出协程对应方法,其后面的代码不会被执行

通过上面的一些yield一些用法以及其在脚本生命周期中的位置,我们也可以看到关于协程不是线程的概念的具体的解释,所有的这些方法都是在主线程中进行的,只是有别于我们正常使用的Update与LateUpdate这些可视的方法

5、协程几个小用法

5.1、将一个复杂程序分帧执行:

如果一个复杂的函数对于一帧的性能需求很大,我们就可以通过yield return null将步骤拆除,从而将性能压力分摊开来,最终获取一个流畅的过程,这就是一个简单的应用

5.3、异步加载等功能

只要一说到异步,就必定离不开协程,因为在异步加载过程中可能会影响到其他任务的进程,这个时候就需要通过协程将这些可能被影响的任务剥离出来

常见的异步操作有:

  • AB包资源的异步加载
  • Reaources资源的异步加载
  • 场景的异步加载
  • WWW模块的异步请求

参考链接:迭代器 - C# | Microsoft Learn

可根据需要提供尽可能多的 yield return 语句来满足方法需求: 

public IEnumerable<int> GetSetsOfNumbers()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    yield return 50;

    index = 100;
    while (index < 110)
        yield return index++;
}

上述所有示例都有一个异步对应项。 在每种情况下,将 IEnumerable<T> 的返回类型替换为 IAsyncEnumerable<T>。 例如,前面的示例将具有以下异步版本:

public async IAsyncEnumerable<int> GetSetsOfNumbersAsync()
{
    int index = 0;
    while (index < 10)
        yield return index++;

    await Task.Delay(500);

    yield return 50;

    await Task.Delay(500);

    index = 100;
    while (index < 110)
        yield return index++;
}

迭代器方法有一个重要限制:在同一方法中不能同时使用 return 语句和 yield return 语句。 以下代码无法编译

有时,正确的做法是将迭代器方法拆分成 2 个不同的方法。 一个使用 return,另一个使用 yield return。 考虑这样一种情况:需要基于布尔参数返回一个空集合,或者返回前 5 个奇数。 可编写类似以下 2 种方法的方法:

public IEnumerable<int> GetSingleDigitOddNumbers(bool getCollection)
{
    if (getCollection == false)
        return new int[0];
    else
        return IteratorMethod();
}

private IEnumerable<int> IteratorMethod()
{
    int index = 0;
    while (index < 10)
    {
        if (index % 2 == 1)
            yield return index;
        index++;
    }
}

看看上面的方法。 第 1 个方法使用标准 return 语句返回空集合,或返回第 2 个方法创建的迭代器。 第 2 个方法使用 yield return 语句创建请求的序列。

参考链接:Unity协程的原理与应用 - 知乎 (zhihu.com)

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.
By default, a coroutine is resumed on the frame after it yields but it is also possible to introduce a time delay using [WaitForSeconds](https://docs.unity3d.com/ScriptReference/WaitForSeconds.html)

简单的说,协程就是一种特殊的函数,它可以主动的请求暂停自身并提交一个唤醒条件,Unity会在唤醒条件满足的时候去重新唤醒协程。

2. 如何使用

MonoBehaviour.StartCoroutine()方法可以开启一个协程,这个协程会挂在该MonoBehaviour下。

MonoBehaviour生命周期的UpdateLateUpdate之间,会检查这个MonoBehaviour下挂载的所有协程,并唤醒其中满足唤醒条件的协程。

要想使用协程,只需要以IEnumerator为返回值,并且在函数体里面用yield return语句来暂停协程并提交一个唤醒条件。然后使用StartCoroutine来开启协程。

思考:协程能做的Update都能做,那为什么我们需要协程呢? 答:使用协程,我们可以把一个跨越多帧的操作封装到一个方法内部,代码会更清晰。

4. 注意事项

  1. 协程是挂在MonoBehaviour上的,必须要通过一个MonoBehaviour才能开启协程。
  2. MonoBehaviour被Disable的时候协程会继续执行,只有MonoBehaviour被销毁的时候协程才会被销毁。
  3. 协程看起来有点像是轻量级线程,但是本质上协程还是运行在主线程上的,协程更类似于Update()方法,Unity会每一帧去检测协程需不需要被唤醒。一旦你在协程中执行了一个耗时操作,很可能会堵塞主线程。这里提供两个解决思路:(1) 在耗时算法的循环体中加入yield return null来将算法分到很多帧里面执行;(2) 如果耗时操作里面没有使用Unity API,那么可以考虑在异步线程中执行耗时操作,完成后唤醒主线程中的协程。

二. Unity协程的底层原理

协程分为两部分,协程与协程调度器:协程仅仅是一个能够中间暂停返回的函数,而协程调度是在MonoBehaviour的生命周期中实现的。 准确的说,Unity只实现了协程调度部分,而协程本身其实就是用了C#原生的”迭代器方法“。

1. 协程本体:C#的迭代器函数

许多语言都有迭代器的概念,使用迭代器我们可以很轻松的遍历一个容器。 但是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
  • 调用IEnumeratorMoveNext()成员,会执行协程直到遇到第一个yield return或者执行完毕。
  • 调用IEnumeratorCurrent成员,可以获得yield return后面接的返回值,该返回值可以是任何类型的对象。

这里有两个要注意的地方:

  1. IEnumerator中的yield return可以返回任意类型的对象,事实上它还有泛型版本IEnumerator<T>,泛型类型的迭代器中只能返回T类型的对象。Unity原生协程使用普通版本的IEnumerator,但是有些项目(比如倩女幽魂)自己造的协程轮子可能会使用泛型版本的IEnumerator<T>
  2. 函数调用的本质是压栈,协程的唤醒也一样,调用IEnumerator.MoveNext()时会把协程方法体压入当前的函数调用栈中执行,运行到yield return后再弹栈。这点和有些语言中的协程不大一样,有些语言的协程会维护一个自己的函数调用栈,在唤醒的时候会把整个函数调用栈给替换,这类协程被称为有栈协程,而像C#中这样直接在当前函数调用栈中压入栈帧的协程我们称之为无栈协程。关于有栈协程和无栈协程的概念我们会在后文四. 跳出Unity看协程中继续讨论
Unity中的协程是无栈协程,它不会维护整个函数调用栈,仅仅是保存一个栈帧。

3. Unity协程的架构

基类:YieldInstruction 其它所有协程相关的类都继承自这个类。Unity的协程只允许返回继承自YieldInstruction的对象或者null。如果返回了其他对象则会被当成null处理。

协程类:Coroutine 你可以通过yield return一个协程来等待一个协程执行完毕,所以Coroutine也会继承自YieldInstruction。 Coroutine仅仅代表一个协程实例,不含任何成员方法,你可以将Coroutine对象传到MonoBehaviour.StopCoroutine方法中去关闭这个协程。

遗憾的是,Unity关于协程的这套都是在C++层实现的并且几乎没有暴露出C#接口,所以扩展起来会比较麻烦。

三. 扩展Unity的协程

这部分看原文

四. 跳出Unity看协程

1. 进程,线程与协程

进程是操作系统资源分配的基本单位 线程是处理器调度与执行的基本单位

这是操作系统书上对进程与线程的抽象描述。具体一点的说,进程其实就是程序运行的实例:程序本身只是存储在外存上的冷冰冰的二进制流,计算机将这些二进制流读进内存并解析成指令和数据然后执行,程序便成为了进程。

每一个进程都独立拥有自己的指令和数据,所以称为资源分配的基本单位。其中数据又分布在内存的不同区域,我们在C语言课程中学习过内存四区的概念,一个运行中的进程所占有的内存大体可以分为四个区域:栈区、堆区、数据区、代码区。其中代码区存储指令,另外三个区存储数据。

线程是处理器调度和执行的基本单位,一个线程往往和一个函数调用栈绑定,一个进程有多个线程,每个线程拥有自己的函数调用栈,同时共用进程的堆区,数据区,代码区。操作系统会不停地在不同线程之间切换来营造出一个并行的效果,这个策略称为时间片轮转法。

那么协程在其中又处于什么地位呢? 一切用户自己实现的,类似于线程的轮子,都可以称之为是协程。

C#中的迭代器方法是协程; Unity在迭代器的基础上扩展出来的协程模块是协程; 你在操作系统实验中模仿线程自己写出来的"线程"也是协程; ........

协程有什么样的行为,完全由实现协程的程序员来决定(线程和进程都是操作系统中写死的),这就导致了不同开发框架下的协程差别很大。有的协程有自己的函数调用栈,有的协程共用线程的函数调用栈;有的协程是单线程上的,有的协程可以多线程调度;有的协程和线程是一对多的关系,有的协程和线程是多对多的关系。

操作系统可以有多个进程 一个进程对应一个或多个线程 线程和协程的对应关系,由具体的开发框架决定

2. 不同框架下协程的共同点

虽然不同开发框架下的协程各不一样,但是这些协程基本上还是有一些共性的

(1) 协程有yield和resume操作

协程可以通过yield操作挂起,通过resume操作恢复。yield一般是协程主动调用,resume一般是调度器调用。 大多数协程库都支持这两个操作,无非是可能API的名字不一样。 比如C#中,resume操作就是MoveNext

(2) 协程调度是非抢占式的

线程调度是抢占式的:操作系统会主动中断当前执行中的线程,然后把CPU控制权交给别的线程,就好像有很多线程去争抢CPU的控制权一样。

协程调度是非抢占式的:协程需要主动调用yield来释放CPU控制权,协程的运行中间不会被系统中断打断。

可以看看这个:(扩展)

浅谈倩女手游中的资源更新 - 知乎 (zhihu.com)

IFramework/Example-Readme/Bind.md at master · OnClick9927/IFramework (github.com)

参考文章:当我们在说协程时,我们在说些什么? - Lyon Gu - 博客园 (cnblogs.com)

协程可以将一个方法,放到多个帧内执行,在很大程度上提高了性能。但协程也是有缺陷的:

  1. 不支持返回值;
  2. 不支持异常处理;
  3. 不支持泛型;
  4. 不支持锁;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1488790.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python lambda表达式(匿名函数)

lambda 表达式 在Python中&#xff0c;匿名函数&#xff08;也称为lambda函数&#xff09;是一种简洁的方式来定义小函数&#xff0c;这些函数可以在需要的地方直接定义和使用&#xff0c;而不需要使用def关键字来定义一个具有名称的函数。 lambda 函数是一种小型、匿名的、内…

vue+element ui上传图片到七牛云服务器

本来打算做一个全部都是前端完成的资源上传到七牛云的demo&#xff0c;但是需要获取token&#xff0c;经历了九九八十一难&#xff0c;最终还是选择放弃&#xff0c;token从后端获取&#xff08;springboot&#xff09;。如果你们有前端直接能解决的麻烦记得私我哦&#xff01;…

学习网络编程No.12【传输层协议之TCP】

引言&#xff1a; 北京时间&#xff1a;2024/2/27/14:12&#xff0c;不知过了多久终于在今天上午更新了新的文章。促使好久没有登录CSDN的我回关了几个近期关注我的人&#xff0c;然后过了没多久有人就通过二维码加了我的微信&#xff0c;他问了我一个问题&#xff0c;如何学好…

【S32DS报错】-7-程序进入HardFault_Handler,无法正常运行

【S32K3_MCAL从入门到精通】合集&#xff1a; S32K3_MCAL从入门到精通https://blog.csdn.net/qfmzhu/category_12519033.html 问题背景&#xff1a; 在S32DS IDE中使用PEmicro&#xff08;Multilink ACP&#xff0c;Multilink Universal&#xff0c;Multilink FX&#xff09…

3分钟,学会一个测试员必懂 Lambda 小知识!

今天再来给大家介绍下函数式接口和方法引用。 函数式接口 问&#xff1a;Lambda 表达式的类型是什么&#xff1f; 答&#xff1a;函数式接口 问&#xff1a;函数式接口是什么&#xff1f; 答&#xff1a;只包含一个抽象方法的接口&#xff0c;称为函数式接口 &#xff08;…

【图像版权】论文阅读:CRMW 图像隐写术+压缩算法

不可见水印 前言背景介绍ai大模型水印生成产物不可见水印CRMW 在保护深度神经网络模型知识产权方面与现有防御机制有何不同&#xff1f;使用图像隐写术和压缩算法为神经网络模型生成水印数据集有哪些优势&#xff1f;特征一致性训练如何发挥作用&#xff0c;将水印数据集嵌入到…

MSCKF5讲:后端代码分析

MSCKF5讲&#xff1a;后端代码分析 文章目录 MSCKF5讲&#xff1a;后端代码分析1 初始化initialize()1.1 加载参数1.2 初始化IMU连续噪声协方差矩阵1.3 卡方检验1.4 接收与订阅话题createRosIO() 2 IMU静止初始化3 重置resetCallback()4 featureCallback4.1 IMU初始化判断4.2 I…

YOLOv9独家改进|动态蛇形卷积Dynamic Snake Convolution与RepNCSPELAN4融合

专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;主力高效涨点&#xff01;&#xff01;&#xff01; 一、改进点介绍 Dynamic Snake Convolution是一种针对细长微弱的局部结构特征与复杂多变的全局形态特征设计的卷积模块。 RepNCSPELAN4是YOLOv9中的特…

【C语言】动态内存管理------常见错误,以及经典笔试题分析,柔性数组【图文详解】

欢迎来CILMY23的博客喔&#xff0c;本篇为【C语言】动态内存管理------常见错误&#xff0c;以及经典笔试题分析&#xff0c;柔性数组【图文详解】&#xff0c;感谢观看&#xff0c;支持的可以给个一键三连&#xff0c;点赞关注收藏。 前言 在了解完内存操作中最关键的一节---动…

怎样裁剪视频上下多余的部分?分享3个裁剪的工具!

在数字时代&#xff0c;视频已成为我们生活中不可或缺的一部分。无论是观看电影、制作个人vlog&#xff0c;还是进行专业的视频编辑&#xff0c;我们时常会遇到需要裁剪视频上下多余部分的情况。那么&#xff0c;如何进行视频裁剪呢&#xff1f;本文将为您详细介绍几种常用的视…

day46_Servlet

今日内容 0 复习昨日 1 Servlet基础 1.1 Servlet介绍 1.2 第一个Servlet 1.3 流程分析 1.4 使用细节 1.5 映射细节 1.6 生命周期 2 HttpServlet 2.1 HTTP请求、响应、状态码 2.2 GET和POST的区别 2.3 HttpServlet 0 复习昨日 1 maven创建-java项目结构 2 maven创建-javaweb项目…

16.网络游戏逆向分析与漏洞攻防-网络通信数据包分析工具-设计数据发送结构实现更复杂的数据发送

上一个内容&#xff1a;15.发送通信数据包至分析工具 码云地址&#xff08;master 分支&#xff09;&#xff1a;https://gitee.com/dye_your_fingers/titan 码云版本号&#xff1a;f691a6a12ab49a711713f8ccdc8dd712c05826e9 代码下载地址&#xff0c;在 titan 目录下&…

京东商品优惠券API获取商品到手价

item_get_app-获得JD商品详情原数据 公共参数 请求地址: jd/item_get_app 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥api_nameString是API接口名称&#xff08;包括在请求地址中&#xff09;[item_search,i…

Git 指令深入浅出【2】—— 分支管理

Git 指令深入浅出【2】—— 分支管理 分支管理1. 常用分支管理指令2. 合并分支合并冲突合并模式 3. 实战演习 分支管理 1. 常用分支管理指令 # 查看本地分支 git branch# 查看远程分支 git branch -r# 查看全部分支 git branch -aHEAD 指向的才是当前的工作分支 # 查看当前分…

源码的角度分析Vue2数据双向绑定原理

什么是双向绑定 我们先从单向绑定切入&#xff0c;其实单向绑定非常简单&#xff0c;就是把Model绑定到View&#xff0c;当我们用JavaScript代码更新Model时&#xff0c;View就会自动更新。那么双向绑定就可以从此联想到&#xff0c;即在单向绑定的基础上&#xff0c;用户更新…

win中删除不掉的文件,火绒粉碎删除亲测有效

看网上的 win R 然后终端输入什么删除的&#xff0c;照做了都没有删掉 有火绒的可以试试&#xff1a; 拖进去就删掉了 很好使

开源项目_代码生成项目介绍

1 CodeGeeX 系列 1.1 CodeGeeX 项目地址&#xff1a;https://github.com/THUDM/CodeGeeX 7.6k Star主要由 Python 编写深度学习框架是 Mindspore代码约 2.5W 行有 Dockerfile&#xff0c;可在本地搭建环境模型大小为 150 亿参数相对早期的代码生成模型&#xff0c;开放全部代…

【PCL】 (十六)点云距离图可视化

&#xff08;十六&#xff09;点云距离图可视化 以下代码实现点云及其对应距离图的可视化。 数据样例&#xff1a;sphere100.pcd range_image_visualization.cpp #include <iostream>#include <pcl/range_image/range_image.h> #include <pcl/io/pcd_io.h&g…

CHI协议学习

原始文档&#xff1a;https://developer.arm.com/documentation/102407/0100/?langen CHI 总线拓扑结构 CHI总线拓扑是实现自定义的&#xff0c;可以是RING/MESH/CROSSBAR的类型&#xff1b; RING 一般适用于中等规模芯片MESH 一般适用于大规模芯片CROSSBAR 一般适用于小规模…

30天JS挑战(第十五天)------本地存储菜谱

第十五天挑战(本地存储菜谱) 地址&#xff1a;https://javascript30.com/ 所有内容均上传至gitee&#xff0c;答案不唯一&#xff0c;仅代表本人思路 中文详解&#xff1a;https://github.com/soyaine/JavaScript30 该详解是Soyaine及其团队整理编撰的&#xff0c;是对源代…