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

2024-03-05 00:52
文章标签 到底 unity 协程 coroutine

本文主要是介绍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];elsereturn 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直到遇到第一个yieldSystem.Console.WriteLine(it.Current);//输出1Console.ReadKey();it.MoveNext();//执行Test直到遇到第二个yieldSystem.Console.WriteLine(it.Current);//输出2Console.ReadKey();it.MoveNext();//执行Test直到遇到第三个yieldSystem.Console.WriteLine(it.Current);//输出test3Console.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. 不支持锁;

这篇关于Unity 协程(Coroutine)到底是什么?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/774811

相关文章

使用协程实现高并发的I/O处理

文章目录 1. 协程简介1.1 什么是协程?1.2 协程的特点1.3 Python 中的协程 2. 协程的基本概念2.1 事件循环2.2 协程函数2.3 Future 对象 3. 使用协程实现高并发的 I/O 处理3.1 网络请求3.2 文件读写 4. 实际应用场景4.1 网络爬虫4.2 文件处理 5. 性能分析5.1 上下文切换开销5.2 I/O 等待时间 6. 最佳实践6.1 使用 as

Unity Post Process Unity后处理学习日志

Unity Post Process Unity后处理学习日志 在现代游戏开发中,后处理(Post Processing)技术已经成为提升游戏画面质量的关键工具。Unity的后处理栈(Post Processing Stack)是一个强大的插件,它允许开发者为游戏场景添加各种视觉效果,如景深、色彩校正、辉光、模糊等。这些效果不仅能够增强游戏的视觉吸引力,还能帮助传达特定的情感和氛围。 文档

【H2O2|全栈】Markdown | Md 笔记到底如何使用?【前端 · HTML前置知识】

Markdown的一些杂谈 目录 Markdown的一些杂谈 前言 准备工作 认识.Md文件 为什么使用Md? 怎么使用Md? ​编辑 怎么看别人给我的Md文件? Md文件命令 切换模式 粗体、倾斜、下划线、删除线和荧光标记 分级标题 水平线 引用 无序和有序列表 ​编辑 任务清单 插入链接和图片 内嵌代码和代码块 表格 公式 其他 源代码 预

Unity协程搭配队列开发Tips弹窗模块

概述 在Unity游戏开发过程中,提示系统是提升用户体验的重要组成部分。一个设计良好的提示窗口不仅能及时传达信息给玩家,还应当做到不干扰游戏流程。本文将探讨如何使用Unity的协程(Coroutine)配合队列(Queue)数据结构来构建一个高效且可扩展的Tips弹窗模块。 技术模块介绍 1. Unity协程(Coroutines) 协程是Unity中的一种特殊函数类型,允许异步操作的实现

Unity 资源 之 Super Confetti FX:点亮项目的璀璨粒子之光

Unity 资源 之 Super Confetti FX:点亮项目的璀璨粒子之光 一,前言二,资源包内容三,免费获取资源包 一,前言 在创意的世界里,每一个细节都能决定一个项目的独特魅力。今天,要向大家介绍一款令人惊艳的粒子效果包 ——Super Confetti FX。 二,资源包内容 💥充满活力与动态,是 Super Confetti FX 最显著的标签。它宛如一位

Unity数据持久化 之 一个通过2进制读取Excel并存储的轮子(4)

本文仅作笔记学习和分享,不用做任何商业用途 本文包括但不限于unity官方手册,unity唐老狮等教程知识,如有不足还请斧正​​ Unity数据持久化 之 一个通过2进制读取Excel并存储的轮子(3)-CSDN博客  这节就是真正的存储数据了   理清一下思路: 1.存储路径并检查 //2进制文件类存储private static string Data_Binary_Pa

Unity Adressables 使用说明(一)概述

使用 Adressables 组织管理 Asset Addressables 包基于 Unity 的 AssetBundles 系统,并提供了一个用户界面来管理您的 AssetBundles。当您使一个资源可寻址(Addressable)时,您可以使用该资源的地址从任何地方加载它。无论资源是在本地应用程序中可用还是存储在远程内容分发网络上,Addressable 系统都会定位并返回该资源。 您

Unity Adressables 使用说明(六)加载(Load) Addressable Assets

【概述】Load Addressable Assets Addressables类提供了加载 Addressable assets 的方法。你可以一次加载一个资源或批量加载资源。为了识别要加载的资源,你需要向加载方法传递一个键或键列表。键可以是以下对象之一: Address:包含你分配给资源的地址的字符串。Label:包含分配给一个或多个资源的标签的字符串。AssetReference Obj

在Unity环境中使用UTF-8编码

为什么要讨论这个问题         为了避免乱码和更好的跨平台         我刚开始开发时是使用VS开发,Unity自身默认使用UTF-8 without BOM格式,但是在Unity中创建一个脚本,使用VS打开,VS自身默认使用GB2312(它应该是对应了你电脑的window版本默认选取了国标编码,或者是因为一些其他的原因)读取脚本,默认是看不到在VS中的编码格式,下面我介绍一种简单快

Unity数据持久化 之 一个通过2进制读取Excel并存储的轮子(3)

本文仅作笔记学习和分享,不用做任何商业用途 本文包括但不限于unity官方手册,unity唐老狮等教程知识,如有不足还请斧正​​ Unity数据持久化 之 一个通过2进制读取Excel并存储的轮子(2) (*****生成数据结构类的方式特别有趣****)-CSDN博客 做完了数据结构类,该做一个存储类了,也就是生成一个字典类(只是声明)  实现和上一节的数据结构类的方式大同小异,所