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

参考链接: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. 不支持锁;

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

(MATLAB)第十二章-数列与极限

目录 12.1 数列 12.1.1 数列求和 1. 累计求和函数sum() 2. 忽略NaN累计求和函数 nansum() 3. 求此元素位置之前的元素和函数cumsum() 4. 求梯形累计和函数cumtrapz() 12.1.2 数列求积 1. 元素连续相乘函数 prod() 2. 求累计积函数 cumprod() 3. 阶乘函数 ffactorial(n…

bun build

Bun 的快速原生打包器现已进入测试版阶段。可通过 bun build CLI 命令或 Bun.build() JavaScript API 使用。 bun build ./index.tsx --outdir ./build await Bun.build({entrypoints: [./index.tsx],outdir: ./build, }); 速度很快。下面的数字代表 esbuild 在 three.js 基…

Crossbar阵列的电路结构及其基本原理

忆阻器Crossbar阵列是一种先进的神经网络硬件实现技术&#xff0c;它利用忆阻器的物理特性来模拟神经网络中的突触连接&#xff0c;为人工智能和机器学习应用提供了一种高效、低能耗的计算平台。本文将深入探讨忆阻器Crossbar阵列的基本原理及其在Read&#xff08;读取&#xf…

Studio One 6永久激活版 附完整图文安装破解教程

Studio One 6是一款功能强大的音乐制作和录音软件&#xff0c;专为Mac操作系统设计。它提供了多轨录音和混音、MIDI音乐制作、实时效果和处理、VST插件支持以及高级编辑和编排等丰富的功能。无论是专业音乐制作人还是音乐爱好者&#xff0c;都可以使用Studio One 6来创建和编辑…

Android m/mm/mmm/make编译模块

一.编译成模块的前置条件 Android编译环境初始化完成后&#xff0c;我们就可以用m/mm/mmm/make命令编译源代码了。lunch命令其实是定义在build/envsetup.sh文件中的函数lunch提供的。与lunch命令一样&#xff0c;m、mm和mmm命令也分别是由定义在build/envsetup.sh文件中的函数…

istio pod不启动及访问报RBAC错误问题解决

istio pod不启动问题解决 在kubernetes集群中安装istio之后&#xff0c;在创建的depoyment中已经使用了注入注解sidecar.istio.io/inject: true’配置&#xff0c;但是istio pod不创建&#xff0c;代码示例如下 kind: Deployment apiVersion: apps/v1 metadata:name: name-an…

Linux 操作系统概述

GNU计划 GNU --"GNUs Not UNIX" 建立一个自由、开放的UNIX操作系统&#xff08;Free UNIX&#xff09; GNU 通用公共许可证&#xff08;General Public License&#xff0c;GPL&#xff09; ”四项基本自由“ 按照自己的意愿自由地运行该软件自由地学习并根据…

高级大数据技术 实验一 scala编程

​ 高级大数据技术 实验一 scala编程 写的不是很好&#xff0c;大家多见谅&#xff01; 1. 计算水仙花数 实验目标; &#xff08;1&#xff09; 掌握scala的数组&#xff0c;列表&#xff0c;映射的定义与使用 &#xff08;2&#xff09; 掌握scala的基本编程 实验说明 …

【系统需求分析报告-项目案例直接套用】

软件需求分析报告 软件开发要求项目建设内容物理设计安全系统设计安全网络安全设计应用安全设计用户安全管理性能设计稳定性设计安全性设计兼容性设计易操作性设计可维护行设计 软件开发全套精华资料过去进主页领取。

博弈论实用原理浅谈及题目实战【算法竞赛】

一、前言 本篇记录博弈论一些常见原理、做题技巧。 之前没有了解学习过博弈论&#xff0c;这篇文章可以当作记录学习笔记了。 二、初识博弈论 博弈论题目在竞赛中我感觉其实并不少见&#xff0c;只是需要技巧性很强&#xff0c;找到规律打代码很简单&#xff0c;而找不到基本上…

go语言基础 -- 面向对象 -- 接口与多态

接口定义与基本使用 - interface go语言中&#xff0c;接口类型可以定义一组方法&#xff0c;不需要在接口定义中实现方法&#xff0c;并且interface中不能含有变量&#xff0c;如果某个自定义类型要使用时再实现接口的方法。 golang中的接口不需要显式地实现&#xff0c;只要…

秘密共享差分隐私原理解析

1. 隐私计算全貌 &#xfffc;&#xfffc; 可以看到&#xff0c;隐私计算技术从1979年就开始了&#xff0c;历经四代从安全多方计算(MPC)、到差分隐私(DP)、到集中加密技术(TEE)&#xff0c;再到联邦学习(FL)。 2. 秘密共享 secret Sharing 就是“秘密分享”或者“秘密共享”…

“互动+消费”时代,借助华为云GaussDB重构新零售中消费逻辑

场与人的关系 “人—货—场”是零售中重要的三要素&#xff0c;我们一直在追求&#xff0c;将零售中的人、货、场进行数字化并在云端进行整合&#xff0c;形成属于我们自己的云平台。 随着互联网技术为信息提供的便利&#xff0c;消费者的集体力量正在逐渐形成一股强大的反向…

Applied Energy+C论文复现:考虑泊位分配灵活性的港口综合能源系统优化调度程序代码!

程序结合了港口独特的工作属性&#xff0c;构建了泊位优化分配的模型&#xff0c;提出了考虑泊位优化和多能协同的港口综合能源运行优化模型。港口运营商根据多种能源供应的成本特性决策船舶停泊的开始&#xff0f;结束时间&#xff0c;改变港口的总负荷需求曲线。程序算例丰富…

使用postman测试若依登录接口API-2

请求方式 由于登录控制器可知&#xff1a;该请求方式为Post请求 请求地址 在请求路径栏输入请求地址&#xff0c;如下图所示&#xff1a; 参数体 在Body键入所需参数&#xff0c;类型选择raw,数据格式选择"JSON"&#xff1a;如下图所示&#xff1a; 认证成功与失败…

特征值和特征向量及其在机器学习中的应用

特征值和特征向量是线性代数中的概念&#xff0c;用于分析和理解线性变换&#xff0c;特别是由方阵表示的线性变换。它们被用于许多不同的数学领域&#xff0c;包括机器学习和人工智能。 在机器学习中&#xff0c;特征值和特征向量用于表示数据、对数据执行操作以及训练机器学…

NOIP 2009普及组初赛试题及解析

NOIP 2009普及组初赛试题及解析 一. 单项选择题 &#xff08;共20题&#xff0c;每题1.5分&#xff0c;共计30分。每题有且仅有一个正确答案.&#xff09;。二. 问题求解&#xff08;共2题&#xff0c;每题5分&#xff0c;共计10分&#xff09;三. 阅读程序写结果&#xff08;共…

Vue.js 深度解析:模板编译原理与过程

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

网络学习:SMart link技术与Monitor link技术

目录 一、SMart link技术 1.1、SMart link技术简介 1.2、SMart link技术原理及基础知识点 1、应用场景&#xff08;举例&#xff09;&#xff1a; 2、运行机制 3、保护vlan 4、控制VLAN 5、Flush报文 6、SMart link的负载分担机制 7、SMart link角色抢占模式 二、Mo…

YOLOv5目标检测学习(1):yolo系列算法的基础概念

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、基于深度学习的目标检测需要哪些步骤&#xff1f;二、数据准备&#xff08;即准备数据集&#xff09;1.目标检测的数据集如何获取&#xff1f;2.数据集包括…