Unity基础学习(七)Mono中的重要内容(3)协同程序的本质

发布于:2025-05-24 ⋅ 阅读:(16) ⋅ 点赞:(0)

       

目录

一、协程的本质

二、协程本体是迭代器方法的体现

工作原理

访问规则

三、手动实现一个协程调度器


 

        前面我们学习了协程的基本使用方法,你可否有想过他底层的工作原理是什么样的呢?

一、协程的本质

        前面我们实际上已经说了,协同的作用就是将程序分时分步执行。允许将一个任务拆分成多个步骤,在不同帧或特定条件下执行。其本质分为两部分:

(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:迭代已结束,后续调用无效。

工作原理
  1. 首次调用 MoveNext():执行从方法开头到第一个 yield return 之间的代码。

  2. 后续调用:从上一次 yield return 之后继续执行,直到下一个 yield return 或方法结束。

  3. 结束时:最后一次 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 后访问有效值。

访问规则
  1. 在首次调用 MoveNext() 前访问 Current,会得到 null 或未定义值。

  2. 最后一次 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);
                }
            }
        }
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到