Unity基础学习(七)Mono中的重要内容(3)协同程序的本质
目录
一、协程的本质
二、协程本体是迭代器方法的体现
工作原理
访问规则
三、手动实现一个协程调度器
前面我们学习了协程的基本使用方法,你可否有想过他底层的工作原理是什么样的呢?
一、协程的本质
前面我们实际上已经说了,协同的作用就是将程序分时分步执行。允许将一个任务拆分成多个步骤,在不同帧或特定条件下执行。其本质分为两部分:
(1)协程函数本体
协程函数是一个用 IEnumerator 定义的迭代器方法,内部通过 yield return 语句分阶段暂停执行。
(2)协程调度器
Unity 内部实现了协程调度器,负责在合适的时机(如帧更新、等待时间结束等)恢复协程的执行。调度器会根据 yield return 返回的对象决定何时继续执行协程。
其特点是:
非抢占式:协程主动通过 yield 暂停,而不是被系统中断。
单线程:协程在 Unity 主线程运行,无需处理多线程同步问题。
生命周期:与 MonoBehaviour 绑定,对象销毁时协程自动终止。
二、协程本体是迭代器方法的体现
本标题又该如何理解呢?由于协程本体是定义的迭代器方法,也就是说,他是满足迭代器的语法规则的,而我们实际上yield return 其实是一个语法糖而已,C# 编译器会将迭代器方法编译为一个状态机,每个 yield return
对应一个状态。而它具体是如何工作的呢,主要的关键在IEnumerator接口:包含 MoveNext() 和 Current 属性,用于控制执行流程。
图IEnumerator接口显示
我们先来仔细看看MoveNext()和Current属性是干什么的:
(1)MoveNext,看名字我们就不难猜出,这个函数的作用是推动程序的分步进行,主要作用就是将迭代器的执行点移动到下一个 yield return 的位置。具有bool类型的返回值:
返回值:返回 bool 类型,表示是否还有后续步骤:
true:迭代尚未结束,存在下一个步骤。
false:迭代已结束,后续调用无效。
工作原理
-
首次调用
MoveNext()
:执行从方法开头到第一个yield return
之间的代码。 -
后续调用:从上一次
yield return
之后继续执行,直到下一个yield return
或方法结束。 -
结束时:最后一次
MoveNext()
返回false
,且Current
会被重置为null
。
例如:
IEnumerator NumberGenerator()
{Debug.Log("Start");yield return 1; // 第一次 MoveNext() 执行到这里Debug.Log("Step 1");yield return 2; // 第二次 MoveNext() 执行到这里Debug.Log("End");
}void Test()
{IEnumerator iterator = NumberGenerator();iterator.MoveNext(); // 输出 "Start",Current = 1iterator.MoveNext(); // 输出 "Step 1",Current = 2iterator.MoveNext(); // 输出 "End",返回 false,Current = null
}
(2)Current,获取最近一次yield return返回的对象,只能在 MoveNext() 返回 true 后访问有效值。
访问规则
-
在首次调用
MoveNext()
前访问Current
,会得到null
或未定义值。 -
最后一次
MoveNext()
返回false
后,Current
会被重置为null
。
例如:
IEnumerator DataFlow()
{yield return "A";yield return new WaitForSeconds(1);yield return 100;
}void Test()
{IEnumerator ie = DataFlow();while (ie.MoveNext()){Debug.Log(ie.Current); // 输出顺序:// "A" → WaitForSeconds(1) → 100}
}
Unity 协程调度器的伪代码逻辑
// Unity 内部简化逻辑
class CoroutineScheduler
{List<IEnumerator> activeCoroutines = new List<IEnumerator>();void Update() {foreach (var coroutine in activeCoroutines){if (coroutine.MoveNext()) {object yieldObj = coroutine.Current;// 根据 yieldObj 类型决定何时再次执行:// - null → 下一帧继续// - WaitForSeconds → 计时结束后继续// - WaitForFixedUpdate → 物理帧后继续}else {// 移除已完成的协程}}}
}
小结:
特性 | MoveNext() | Current |
---|---|---|
类型 | 方法(返回 bool ) | 属性(返回 object ) |
主要作用 | 推进迭代器到下一个 yield 位置 | 获取当前 yield return 返回的对象 |
调用时机 | 必须主动调用以推进迭代器 | 仅在 MoveNext() 返回 true 后有效 |
典型返回值 | true (未结束)/false (已结束) | yield return 后的对象或 null |
Unity 中的角色 | 由协程调度器自动调用 | 用于判断协程暂停条件(如等待时间) |
你可以简化理解迭代器函数
C#看到的迭代器函数yield return 语法糖,就会把原本是一个的函数 变成几部分,我们就可以通过迭代器 从上到下 遍历这几部分进行执行,就达到了将一个函数中的逻辑分时执行的目的
而协程调度器就是 利用迭代器函数返回的内容来进行之后的处理
比如 unity中的协程调度器,根据yield return 返回的内容 决定了下一次在何时继续执行迭代器函数中的下一部分
理论上来说 我们可以自己利用迭代器的特点 自己实现协程调度器来取代unity自带的调度器
三、手动实现一个协程调度器
首先,有一个对象,这个对象需要具备两个内容,一个是记录下次需要执行的迭代器接口,一个是记录下次执行点条件(这里我们以时间作为条件示例)
public class YieldReturnTime
{//记录 下次还要执行的 迭代器接口public IEnumerator ie;//记录 下次执行的时间点public float time;
}
然后用一个函数记录所有的迭代器接口,方便后续分步执行:
public void MyStartCoroutine(IEnumerator ie){//来进行 分步走 分时间执行的逻辑//传入一个 迭代器函数返回的结构 那么应该一来就执行它//一来就先执行第一步 执行完了 如果返现 返回的true 证明 后面还有步骤if(ie.MoveNext()){//判断 如果yield return返回的是 数字 是一个int类型 那就证明 是需要等待n秒继续执行if(ie.Current is int){//按思路 应该把 这个迭代器函数 和它下一次执行的时间点 记录下来//然后不停检测 时间 是否到达了 下一次执行的 时间点 然后就继续执行它YieldReturnTime y = new YieldReturnTime();//记录迭代器接口y.ie = ie;//记录时间y.time = Time.time + (int)ie.Current;//把记录的信息 记录到数据容器当中 因为可能有多个协程函数 开启 所以 用一个 list来存储list.Add(y);}}}
记录完所有的可执行接口后,然后根据我们自定义的规则,进行调度
void Update(){//为了避免在循环的时候 从列表里面移除内容 我们可以倒着遍历for (int i = list.Count - 1; i >= 0; i--){//判断 当前该迭代器函数 是否到了下一次要执行的时间//如果到了 就需要执行下一步了if( list[i].time <= Time.time ){if(list[i].ie.MoveNext()){//如果是true 那还需要对该迭代器函数 进行处理//如果是 int类型 证明是按秒等待if(list[i].ie.Current is int){list[i].time = Time.time + (int)list[i].ie.Current;}else{//该list 只是存储 处理时间相关 等待逻辑的 迭代器函数的 //如果是别的类型 就不应该 存在这个list中 应该根据类型把它放入别的容器中list.RemoveAt(i);}}else{//后面已经没有可以等待和执行的了 证明已经执行完毕了逻辑list.RemoveAt(i);}}}}
附完整代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class YieldReturnTime
{//记录 下次还要执行的 迭代器接口public IEnumerator ie;//记录 下次执行的时间点public float time;
}public class CoroutineMgr : MonoBehaviour
{/*实现单例*/private static CoroutineMgr instance;public static CoroutineMgr Instance => instance;//申明存储 迭代器函数对象的 容器 用于 一会继续执行private List<YieldReturnTime> list = new List<YieldReturnTime>();// Start is called before the first frame updatevoid Awake(){instance = this;}public void MyStartCoroutine(IEnumerator ie){//来进行 分步走 分时间执行的逻辑//传入一个 迭代器函数返回的结构 那么应该一来就执行它//一来就先执行第一步 执行完了 如果返现 返回的true 证明 后面还有步骤if(ie.MoveNext()){//判断 如果yield return返回的是 数字 是一个int类型 那就证明 是需要等待n秒继续执行if(ie.Current is int){//按思路 应该把 这个迭代器函数 和它下一次执行的时间点 记录下来//然后不停检测 时间 是否到达了 下一次执行的 时间点 然后就继续执行它YieldReturnTime y = new YieldReturnTime();//记录迭代器接口y.ie = ie;//记录时间y.time = Time.time + (int)ie.Current;//把记录的信息 记录到数据容器当中 因为可能有多个协程函数 开启 所以 用一个 list来存储list.Add(y);}}}// Update is called once per framevoid Update(){//为了避免在循环的时候 从列表里面移除内容 我们可以倒着遍历for (int i = list.Count - 1; i >= 0; i--){//判断 当前该迭代器函数 是否到了下一次要执行的时间//如果到了 就需要执行下一步了if( list[i].time <= Time.time ){if(list[i].ie.MoveNext()){//如果是true 那还需要对该迭代器函数 进行处理//如果是 int类型 证明是按秒等待if(list[i].ie.Current is int){list[i].time = Time.time + (int)list[i].ie.Current;}else{//该list 只是存储 处理时间相关 等待逻辑的 迭代器函数的 //如果是别的类型 就不应该 存在这个list中 应该根据类型把它放入别的容器中list.RemoveAt(i);}}else{//后面已经没有可以等待和执行的了 证明已经执行完毕了逻辑list.RemoveAt(i);}}}}
}