Unity协程的定义、使用及原理,与线程的区别、缺点全方面解析

2023-12-23 17:55:17

目录

协程的定义及简介

协程的用途

定时器

将复杂程序分帧执行

等待某些条件完成后执行后续

异步加载资源

协程的原理

MonoBehaviour中每一帧的游戏循环

迭代器 IEnumerator 接口

具体执行过程

协程和线程的区别

协程的缺点

无法返回值

依赖于MonoBehaviour

维护困难与回调地狱


协程的定义及简介

先来看看Unity官方给出的定义:

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.

“协程类似于一个具有暂停执行并将控制权返回给Unity的功能的函数,但随后可以在下一帧继续从中断处继续执行。”

Unity中的协程是一种特殊的函数,它允许在执行过程中在特定点暂停,并在以后的时间点(如下一帧)从暂停的地方继续执行。这使得协程成为处理延时、等待和异步操作的强大工具。同时可以避免在主线程上进行长时间的阻塞操作。

协程的用途

定时器

当你需要延时执行一个方法或者是每隔一段时间就执行某项操作时,可以使用协程。

比如? ? ? ? yield return new WaitForSeconds(.1f),可以使得协程

以下是官方的案例:

游戏中的许多任务需要定期执行,最容易想到的方法是将任务包含在 Update 函数中。但是,通常情况下,每秒将多次调用该函数。不需要以这样的频繁程度重复任务时,可以将其放在协程中来进行定期更新,而不是每一帧都更新。这方面的一个示例可能是在附近有敌人时向玩家发出的警报。此代码可能如下所示:
?

bool ProximityCheck()
{
    for (int i = 0; i < enemies.Length; i++)
    {
        if (Vector3.Distance(transform.position, enemies[i].transform.position) < dangerDistance) {
                return true;
        }
    }

    return false;
}

如果有很多敌人,那么每帧都调用此函数可能会带来很大开销。但是,可以使用协程,每十分之一秒调用一次:

IEnumerator DoCheck()
{
    for(;;)
    {
        if (ProximityCheck())
        {
            // Perform some action here
        }
        yield return new WaitForSeconds(.1f);
    }
}

将复杂程序分帧执行

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

举一个案例,如果某一时刻需要使用Update读取一个列表,这样一般需要一个循环去遍历列表,这样每帧的代码执行量就比较大,就可以将这样的执行放置到协程中来处理:
?

//伪代码
?
IEnumerator ShowImageFromUrl(string url)
{
    Image image = null;
    yield return LoadImageAsync(url, image); //异步加载图像,加载完成后唤醒协程
    Show(image);
}

等待某些条件完成后执行后续

直到变量满足某些条件

         yield return new WaitUntil(()=> counter <= 0);

直到函数返回真

            yield return new WaitUntil(() => true);

直到某个协程开始执行后

            yield return StartCoroutine(Test());

异步加载资源

资源加载指的是通过IO操作,将磁盘或服务器上的数据加载成内存中的对象。资源加载一般是一个比较耗时的操作,如果直接放在主线程中会导致游戏卡顿,通常会放到异步线程中去执行。

举个例子,当你需要从服务器上加载一个图片并显示给用户,你需要做两件事情:

  1. 通过IO操作从服务器上加载图片数据到内存中。
  2. 当加载完成后,将图片显示在屏幕上。

其中,2操作必须等待1操作执行完毕后才能开始执行。

//伪代码
?
IEnumerator ShowImageFromUrl(string url)
{
    Image image = null;
    yield return LoadImageAsync(url, image); //异步加载图像,加载完成后唤醒协程
    Show(image);
}

以下是较为进阶的内容


协程的原理

基于迭代器:Unity协程的核心是C#的迭代器(Iterator)。迭代器允许函数在执行过程中暂停,并在未来某个时刻从上次暂停的地方继续执行。

yield关键字:在协程中,yield关键字用于指示暂停点。当协程运行到yield语句时,它会返回一个值,然后暂停执行,直到Unity决定继续执行这个协程。

协程的队列:Unity在内部维护了一个协程的执行队列。当你调用StartCoroutine()时,Unity会将协程加入这个队列。

MonoBehaviour中每一帧的游戏循环

协程的执行:在每一帧的游戏循环中,Unity会检查这些协程,并根据它们的yield返回值决定是否执行协程中的下一段代码。

在每一帧的游戏循环中可以见下表:

翻阅Unity官方文档中介绍MonoBehaviour生命周期的部分,会发现有很多yield阶段,在这些阶段中,Unity会检查MonoBehaviour中是否挂载了可以被唤醒的协程,如果有则唤醒它。

上面那段话的英文:

If a coroutine has yielded previously but is now due to resume then execution takes place during this part of the update.

“如果一个协程之前已经暂停了,但现在应该恢复执行,那么它的执行将在更新的这一部分进行。”

迭代器 IEnumerator 接口

在C#中,迭代器是通过实现 IEnumerator 接口来实现的。

IEnumerator接口:协程方法被编译成实现了IEnumerator接口的类。这个接口包含:

MoveNext() 方法:这个方法用于将迭代器推进到下一个元素。在协程中,每次调用 MoveNext() 会执行到下一个 yield 语句或协程结束。

Current属性:这个属性用于获取迭代器当前位置的元素。在协程中,它返回的是当前yield语句返回的对象。例如,考虑以下几种情况:

  • yield return null;:这种情况下,协程将在下一帧的Update方法之前恢复执行。Current属性返回的值是null,这告诉Unity协程在下一帧继续执行。

  • yield return new WaitForSeconds(n);:在这里,yield返回的是一个WaitForSeconds对象。Current属性返回的是这个对象,Unity使用它来确定协程应该在等待指定的秒数后继续执行。

Unity根据这个返回值来决定协程的下一步执行时机和方式。

具体执行过程

有了以上的基础知识后,我们就可以总结出协程的具体执行过程:
游戏循环中的执行:Unity内部维护了一个活动协程的队列。当你通过StartCoroutine启动一个协程时,该协程就会被添加到这个队列中。在每一帧的游戏循环中,Unity会遍历协程队列,调用每个协程的MoveNext()方法。在协程中,每次调用 MoveNext() 会执行到下一个 yield?语句,此时,协程执行yield return语句时,它会将控制权交还给Unity引擎,并暂停协程的执行。Unity随后决定何时再次恢复协程,这取决于yield return后的对象。

  • 如果yield return null,协程会在下一帧继续执行。
  • 如果yield return了一个WaitForSeconds对象,Unity会等待指定的时间再继续执行协程。
  • 其他yield return对象(如WaitForEndOfFrameWaitForFixedUpdate)会告诉Unity在特定的游戏循环阶段执行协程。

协程和线程的区别

是否并行执行:Unity的协程在主线程上执行。它们允许你将任务分解成多个步骤或等待,但这些步骤依然是在主线程上顺序执行的。

而线程是可以并行执行的,可以多线程并行执行。

轻量级:协程通常比线程更轻量级,因为它们不需要分配额外的操作系统资源。线程需要分配内存和操作系统线程资源,而协程只需要Unity的调度器来管理。

适用场景:协程适用于处理与游戏对象、Unity的生命周期和Unity API交互相关的任务,如延迟、动画序列、协作动作等。线程更适合处理需要并行执行的计算密集型任务,如物理模拟、复杂的算法计算等。

安全性:协程在Unity中的执行是协作的,可以安全地访问和修改Unity的游戏对象和组件。线程在多线程环境中需要额外的同步措施来确保数据的安全性,容易引入竞态条件和死锁。

调度和等待

  • 协程:协程可以方便地使用yield语句等待一段时间或等待其他协程完成,这使得状态管理和任务调度更容易。
  • 线程:线程通常需要使用诸如锁、信号量等机制来进行线程同步和等待操作,这可能会更复杂。

协程的缺点

无法返回值

使用协程的时候,协程本身无法像常规函数那样直接返回值。

如果你需要在异步操作完成后获取某个计算结果,需要寻找其他方式来传递这个结果。

  1. 使用回调函数:可以在协程结束时调用一个回调函数,并将结果作为参数传递给这个回调函数。

  2. 共享变量:协程可以修改外部定义的变量,这些变量的新值可以在协程结束后被读取。

  3. 事件系统:通过Unity的事件系统,可以在协程完成某项操作时触发事件,并传递相关数据。

依赖于MonoBehaviour

在Unity中,协程通常是在继承自MonoBehaviour的类中启动和运行的。这是因为StartCoroutineStopCoroutine方法是MonoBehaviour类的一部分。

依赖于 MonoBehavior 有什么不好的地方?就是我们在大型的商业项目当中,通常会自己去开发一些游戏框架。我们知道 Unity 为我们提供了脚本的一个基类叫做 MonoBehavior ,但实际上我们在商业项目开发当中很有可能根本就不从MonoBehavior 继承。

对MonoBehaviour依赖的考量

  1. 灵活性和控制MonoBehaviour是Unity提供的一个基础类,它与Unity的生命周期方法紧密绑定(如StartUpdate)。在某些情况下,开发者可能需要更多的控制权和灵活性,比如实现自定义的生命周期管理或更细粒度的控制。

  2. 性能优化MonoBehaviour会随Unity的生命周期进行调用,哪怕是空的生命周期方法也会占用调用时间。在大型项目中,这可能导致性能问题。自定义的游戏框架可以更精确地控制何时和如何执行这些方法。

  3. 代码组织和架构:大型游戏项目往往需要更复杂的代码组织和架构。直接依赖于MonoBehaviour可能限制了项目架构的设计,特别是在实现模块化和解耦方面。

虽然MonoBehaviour提供了许多方便的特性,但在某些情况下,它可能不适合所有的需求,特别是当涉及到高度定制的游戏架构和性能优化时。

维护困难与回调地狱

协程的异步性质可能使得调试变得更加困难。协程中的错误可能不会立即显现,而是在协程的执行过程中的某个点才出现,这可能导致问题的根源不明显。

回调地狱

"回调地狱"(Callback Hell)是一个编程术语,通常用于描述在异步编程中,多层嵌套的回调函数造成的代码结构复杂、难以理解和维护的情况。

在Unity中,当你过度依赖协程来处理复杂的异步逻辑时,可能陷入类似"回调地狱"的困境。具体表现为:

  1. 深层嵌套:一个协程等待另一个协程完成,而那个协程又等待另一个,这样层层嵌套下去,导致代码结构混乱,难以追踪协程之间的逻辑关系。

  2. 逻辑分散:异步逻辑分散在多个协程中,使得理解整个流程变得困难,尤其是当不同协程散布在不同的MonoBehaviour脚本中时。

  3. 维护困难:每增加一个新的异步步骤或更改现有步骤,都可能需要重新考虑整个协程链的结构,这增加了维护和调试的难度。

  4. 错误处理:在嵌套的协程中正确地处理错误和异常可能变得复杂。如果在协程的某个环节发生了错误或异常,那么这个错误信息需要被有效地传递回到协程调用的起点,以便可以被正确处理。在Unity中,当你使用协程处理异步任务或复杂的逻辑流时,你可能会有多个协程相互调用或嵌套。例如,一个协程等待另一个协程完成某个任务。如果在这个过程中的任何一个环节发生了错误(如一个协程中的操作失败),这个错误信息应该被捕获并传递给原始调用者,这样才能对错误做出适当的响应。

为了避免这种“回调地狱”,可以采取以下措施:

  • 逻辑分解:尽量将复杂的协程逻辑分解成更小、更可管理的部分。
  • 避免过深嵌套:避免无必要的嵌套协程,尽可能使用更直接的逻辑流程。
  • 状态机:对于复杂的异步流程,考虑使用状态机来管理状态转换,而不是深层嵌套的协程。
  • 异步/等待模式:在支持C# 6.0及以上版本的Unity项目中,考虑使用异步/等待(async/await)模式,这是一种更现代的异步编程方法,可以提供更清晰的代码结构。

文章来源:https://blog.csdn.net/weixin_43757333/article/details/135166487
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。