目录
前面我们学习了协程的基本使用方法,你可否有想过他底层的工作原理是什么样的呢?
一、协程的本质
前面我们实际上已经说了,协同的作用就是将程序分时分步执行。允许将一个任务拆分成多个步骤,在不同帧或特定条件下执行。其本质分为两部分:
(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 = 1
iterator.MoveNext(); // 输出 "Step 1",Current = 2
iterator.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 update
void 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 frame
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);
}
}
}
}
}