Unity中的协程的原理

知识点补充:

  • 进程 :是操作系统进行资源分配和隔离的基本单位。每个进程拥有独立的虚拟地址空间、数据段、代码段以及系统资源(如打开的文件)。进程间通信(IPC)成本高。
  • 线程 :是操作系统进行 CPU 调度和执行 的基本单位,是进程内的一个子任务。同一进程的多个线程共享进程的内存和资源,因此线程间通信更高效,但同步不当易导致数据错乱。线程的创建、切换由操作系统内核管理。
  • 协程 :是一种用户态 的轻量级线程,其调度完全由用户程序控制 (协作式调度),而非操作系统内核。协程在单线程内实现多任务调度,切换代价极低 ,且能避免无意义的线程阻塞,极大提升并发吞吐量,尤其适合 I/O 密集型场景。在 Java 中,Project Loom 引入的虚拟线程即是协程理念的官方实现。

协程接口:

迭代器接口:

伪代码:

可以看到迭代器和协程的接口实现的方法,但方法的内容执行了哪些操作了,这里直接继承重写一个伪代码来梳理具体的操作逻辑。

cs 复制代码
public class car
{
    public string Brand { get; }
    public int Year { get; }
    public car(string brand, int year)
    {
        Brand = brand;
        Year = year;
    }
    // 打印
    public override string ToString() => $"{Brand} ({Year})";
}
public class cars : IEnumerable
{
    // 存储汽车列表的内部数组
    private car[] carlist;

    // 构造函数:初始化汽车数组
    public cars()
    {
        carlist = new car[6]
        {
            new car("Ford",1992),
            new car("Fiat",1988),
            new car("Buick",1932),
            new car("Ford",1932),
            new car("Dodge",1999),
            new car("Honda",1977)
        };
    }

    // 实现IEnumerable的核心方法:返回迭代器实例
    public IEnumerator GetEnumerator()
    {
        // 把内部的car数组传给自定义迭代器,返回迭代器对象
        return new MyEnumerator(carlist);
    }
}
public class MyEnumerator : IEnumerator
{
    // 持有要遍历的汽车数组(由外部cars类传入)
    public car[] carlist;
    // 核心:状态标记!记录当前遍历到的位置
    int position = -1;

    // 构造函数:接收要遍历的数组
    public MyEnumerator(car[] list)
    {
        carlist = list;
    }

    // IEnumerator核心方法1:推进迭代器状态(协程MoveNext()的原型)
    public bool MoveNext()
    {
        // 状态+1(比如从-1→0,对应第一个元素;0→1对应第二个,以此类推)
        position++;
        // 返回是否还有下一个元素:有则true(继续遍历),无则false(遍历结束)
        return (position < carlist.Length);
    }

    // IEnumerator核心方法2:重置迭代器状态
    public void Reset()
    {
        position = -1; // 回到初始状态
    }

    // IEnumerator核心属性:获取当前状态对应的元素(协程的Current属性原型)
    public object Current
    {
        get
        {
            try
            {
                // 返回当前position对应的汽车对象
                return carlist[position];
            }
            catch (IndexOutOfRangeException)
            {
                // 越界时抛标准异常
                throw new InvalidOperationException();
            }
        }
    }
}


// 使用
var cars = new cars();
// 打印
foreach (var number in cars)
{
    Debug.Log($"Main: {number}");
}

可以看到foreach迭代器,每次迭代器都会调用 IEnumerator GetEnumerator(); 方法,获取协程。协程是会记录更新每次的数组状态,执行返回对应的数组状态的值,这个模式就是状态机的原理。

原理:

unity的协程就是当你在IEnumerator 的函数里写yield时,C# 编译器会自动生成一个状态机类。

yield的实现相对于是状态机的添加不同行为节点的方法。

MoveNext会按照你的添加顺序依次执行节点

  • MoveNext():控制协程 "走下一步",返回true表示还有后续步骤,false表示协程结束;
  • Current:返回当前要等待的对象(比如WaitForSeconds
  • Reset :重置状态。

上面我们是我们实现的伪代码是直接返回值,

cs 复制代码
    public IEnumerable<int> CountUp()
    {
        for (int i = 1; i <= 5; i++)
        {
            yield return i; // 暂停并返回当前值
        }
    }

但实际上不是直接return返回值,而是暂停当前返回值的操作,需要等到下一次**MoveNext** 更新Current需要暂停的对象,旧的Current的对象就恢复返回。

那么核心问题就是什么时候调用**MoveNext, ** 在上面伪代码中直接foreach来调用的movenext,那么问题就是没有等待。

unity的原生协程之所以是运行在主线程并且需要依靠MonoBehaviour的生命周期,是因为untiy的协程管理会在每帧的更新都去查询(引擎每帧轮询 )当前的启动没有完成的协程当前的等待对象是否完成,完成就调用协程的MoveNext方法,没有就不执行操作。

原生协程依靠主线程执行,但是不会阻塞,

举个例子:邮递员需要人在家的时候才能把信交付出去,并且拿到新的信,没有新的信就结束,邮递员会每次查看需要送的信如果人不在家,邮递员不会等,回去下一个地址查看,没有送出的信会第二天继续流程。不会因为等待某人,耽误行程导致不能按时下班或者第二天上班迟到。依然会按时上下班。

简单说:原生协程就是 **"状态机 + 引擎每帧轮询"** 的组合,伪异步执行代码。

问题:

1.因为原生协程是依靠untiy的MonoBehaviour的生命周期实现的轮询,所有纯C#的情况,是无法使用协程的,因为纯C#的代码是上面生命周期来轮询执行协程的。

2.GC 严重:每次创建WaitForSeconds/WaitUntil都会 new 对象,频繁触发垃圾回收;

3.拓展性差:自定义等待条件需要写额外逻辑,无法统一管理。

自定义:

原生协程无法在纯 C# 类中使用,核心原因是调度权完全绑定 Unity 引擎和 MonoBehaviour

  1. 启动层面:StartCoroutine()MonoBehaviour的成员方法,纯 C# 类没有这个方法,无法启动协程;
  2. 驱动层面:Unity 引擎只对挂载了MonoBehaviour的对象管理协程调度,纯 C# 类没有 "每帧执行" 的入口(Update/FixedUpdate),协程的MoveNext()无法被驱动;
  3. 生命周期层面:原生协程的生命周期和所属 MonoBehaviour 绑定,纯 C# 类无生命周期,引擎无法感知其存在。

知道了原生协程的问题,就可以自定义进行解决:

自定义需要实现的目标:

1.适配纯C#的情况,

2.优化GC,可以使用对象池

实现步骤:
1.对象池

为伪代码使用,就不复杂化了。就简单的实现创建和归还功能即可。

cs 复制代码
/// <summary>
/// 简化对象池
/// </summary>
/// <typeparam name="T"></typeparam>
public class ObjectPool<T> where T : new()
{
    private readonly Stack<T> _pool = new Stack<T>();
    private readonly int _maxSize;

    public ObjectPool(int maxSize = 1024)
    {
        _maxSize = maxSize;
    }

    // 获取对象
    public T Get()
    {
        return _pool.Count > 0 ? _pool.Pop() : new T();
    }

    // 释放对象
    public bool Release(T obj)
    {
        if (_pool.Count < _maxSize)
        {
            _pool.Push(obj);
            return true;
        }
        return false;
    }

}
2.协程的等待器

实现两个必须的基本功能

(1)获取是否等待完成的方法。

作用:每次轮询都会触发, 及每次轮询都会执行该方法,更新执行进度。

(2)等待结束的资源释放方法。

作用:就是持有的内容资源,不使用就释放掉。

并且实现一个简单的定时等待的方法。

提供一个暂停返回值直到指定等待时间的方法。

cs 复制代码
/// <summary>
/// 抽象的等待器对象
/// </summary>
public abstract class Waiter
{
    protected internal abstract bool KeepWaiting();   // 是否继续等待
    protected internal abstract void Dispose();              // 释放资源

}






/// <summary>
/// 实现一个时间等待器对象
/// </summary>
public class TimeWaiter : Waiter
{
    private static readonly ObjectPool<TimeWaiter> _pool = new ObjectPool<TimeWaiter>();    // 对象池
    public float Seconds;    // 等待时间
    public float Timer;      // 计时器
    public Func<float> GetDeltaTime; // 获取时间增量的函数,需要有外部赋值返回,使用func委托

    protected internal override bool KeepWaiting()
    {
        Timer += GetDeltaTime();
        return Timer >= Seconds;
    }

    protected internal override void Dispose()
    {
        // 重置状态
        Seconds = 0;
        Timer = 0;
        GetDeltaTime = null;
        // 归还对象池
        _pool.Release(this);
    }

    // 静态工厂方法获取TimeWaiter实例
    public static TimeWaiter WaitTime(float  seconds, Func<float> getDeltaTime)
    {
        // 数据校验
        if (seconds < 0) throw new ArgumentOutOfRangeException(nameof(seconds), "等待时间不能为负数");
        var waiter = _pool.Get();
        waiter.Seconds = seconds;
        waiter.Timer = 0;
        waiter.GetDeltaTime = getDeltaTime;
        return waiter;
    }


}
3.协程的句柄

协程句柄的核心意义可总结为 3 句话:

  1. 简化控制:外部无需关心协程的底层调度器、迭代器,只用句柄就能停止 / 释放协程;
  2. 精准安全:每个句柄对应一个协程,精准控制单个协程,且支持资源清理,避免内存泄漏;
  3. 解耦扩展:统一控制接口,适配不同调度器实现,不随底层逻辑变化而修改外部代码。
cs 复制代码
// 协程的句柄
public struct CoroutineHandle : IDisposable, IEquatable<CoroutineHandle>
{
    private ICoroutineScheduler _scheduler;     // 调度器
    private IEnumerator<Waiter> _coroutine;     // 协程枚举器

    public CoroutineHandle(ICoroutineScheduler scheduler, IEnumerator<Waiter> coroutine)
    {
        _scheduler = scheduler;
        _coroutine = coroutine;
    }

    public void Stop()
    {
        if ( _coroutine != null )
        {
            _scheduler.StopCoroutine(in _coroutine);
            _coroutine = null;
        }
    }



    public void Dispose()
    {
        _coroutine = null;
        _scheduler = null;

    }

    public bool Equals(CoroutineHandle other)
    {
        return _scheduler == other._scheduler && Equals(_coroutine, other._coroutine);

    }

}
4.协程的调度器

实现必要的协程方法

Run:轮询的方法实现。

StopCoroutine:暂停协程的方法实现。

RunCoroutine:启动协程的方法实现。

cs 复制代码
// 调度器接口
public interface ICoroutineScheduler
{
    void Run();

    void StopCoroutine(in IEnumerator<Waiter> coroutine);

    CoroutineHandle RunCoroutine(in IEnumerator<Waiter> coroutine);
}



// 协程的调度器
public class CoroutineScheduler : ICoroutineScheduler
{
    // 记录所有运行中的协程-> 使用链表方便增删操作
    private readonly LinkedList<IEnumerator<Waiter>> _runningCoroutines = new LinkedList<IEnumerator<Waiter>>();


    // 对应原生协程中的Update方法 -> 每帧调用一次/定时调用一次
    public void Run()
    {
        var node = _runningCoroutines.First;    // 获取第一个节点
        while (node != null)
        {
            var coroutine = node.Value;         // 获取协程枚举器
            var nextNode = node.Next; // 先缓存下一个节点,避免移除当前节点后遍历中断

            // 初始状态:Current为null,先调用MoveNext()启动协程
            if (coroutine.Current == null && !coroutine.MoveNext())
            {// 协程为空,并且无法移动到下个状态节点,说明协程结束
                _runningCoroutines.Remove(node);
                // 继续下一个节点
                node = nextNode;
                continue;
            }

            // 协程没有结束 检查当前等待器是否完成等待
            var currentWaiter = coroutine.Current as Waiter; // 获取当前等待器
            if (currentWaiter != null && currentWaiter.KeepWaiting())
            {
                // 结束等待释放资源再移除,跳过到下一个协程
                currentWaiter.Dispose();
                if (!coroutine.MoveNext())
                {
                    // 协程结束,移除
                    _runningCoroutines.Remove(node);
                }
            }

            node = nextNode;

        }
    }

    public CoroutineHandle RunCoroutine(in IEnumerator<Waiter> coroutine)
    {
        // 生成协程句柄
        var handle = new CoroutineHandle(this, coroutine);
        _runningCoroutines.AddLast(coroutine);
        return handle;
    }

    public void StopCoroutine(in IEnumerator<Waiter> coroutine)
    {
        _runningCoroutines.Remove(coroutine);
    }
}
5.使用测试
cs 复制代码
public class PureCSharpClass
{
    private readonly CoroutineScheduler _scheduler = new CoroutineScheduler();
    private float _currentTime;

    // 封装:启动协程的公共方法
    public CoroutineHandle StartCustomCoroutine(IEnumerator<Waiter> coroutine)
    {
        return _scheduler.RunCoroutine(coroutine);
    }

    // 封装:停止协程的公共方法
    public void StopCustomCoroutine(IEnumerator<Waiter> coroutine)
    {
        _scheduler.StopCoroutine(coroutine);
    }

    // 模拟Update
    public void Update(float deltaTime)
    {
        _currentTime += deltaTime;
        _scheduler.Run();
        Debug.Log($"当前时间:{_currentTime:F1}s");
    }

    // 协程逻辑
    public IEnumerator<Waiter> TestCoroutine()
    {
        Debug.Log("纯C#协程启动");
        yield return TimeWaiter.WaitTime(4f, () => 0.01f);
        Debug.Log("纯C#协程等待4秒后执行");
        yield return TimeWaiter.WaitTime(1f, () => 0.01f);
        Debug.Log("纯C#协程结束");
    }
}






// 测试方法

public void Test()
{
    // 自定义协程测试
    var pureClass = new PureCSharpClass();
    // 启动协程
    pureClass.StartCustomCoroutine(pureClass.TestCoroutine());

    // 模拟10帧执行(每帧0.1秒)
    for (int i = 0; i < 1000; i++)
    {
        pureClass.Update(0.01f);
        System.Threading.Thread.Sleep(10); // 模拟帧间隔
    }
}
补充知识点:
1.对struct、abstract class、interface的使用
特性 struct(结构体) abstract class(抽象类) interface(接口)
类型 值类型 引用类型 引用类型(约定)
继承 不支持继承(隐式继承ValueType) 支持单继承 支持多实现
实例化 可实例化 不可实例化 不可实例化
默认实现 可以有 可以有具体和抽象方法 C#8.0+可以有默认实现
构造函数 可有(必须带参数) 可有 无构造函数
字段 可以有 可以有 C#8.0+可以有静态字段
访问修饰符 成员默认private 默认继承规则 默认public
选择指南
何时使用struct?
  • 数据大小小于16字节

  • 需要值语义(拷贝时完全复制)

  • 不需要继承和多态

  • 性能敏感,避免堆分配

  • 数据不可变时更安全

特点:

  • 值类型,栈上分配(通常)

  • 适合小型数据结构

  • 复制时是值拷贝

何时使用abstract class?
  • 多个相关类共享代码

  • 需要提供基础实现

  • 使用模板方法模式

  • 需要控制子类的创建和初始化

  • 需要包含状态(字段)

特点:

  • 为相关类提供共同基类

  • 可包含具体实现

  • 支持模板方法模式

何时使用interface?
  • 定义跨继承体系的能力

  • 需要多重继承

  • 实现松耦合设计

  • 定义服务契约(如依赖注入)

  • 支持测试替身(Mock/Stub)

特点:

  • 定义契约/能力

  • 支持多继承

  • 实现解耦

简单梳理的口诀:
  • 想做 "数据容器 + 简单方法"→ struct;
  • 想做 "基类 + 子类复用逻辑"→ abstract class;
  • 想做 "行为契约 + 解耦扩展"→ interface。
相关推荐
垂葛酒肝汤4 小时前
Unity第一个项目
unity·游戏引擎
Sator15 小时前
Unity的InputSystem常见问题和疑惑解答
java·unity·游戏引擎
郝学胜-神的一滴5 小时前
QtOpenGL多线程渲染方案深度解析
c++·qt·unity·游戏引擎·godot·图形渲染·unreal engine
IMPYLH6 小时前
Lua 的 Table 模块
开发语言·笔记·后端·junit·游戏引擎·lua
Howrun7771 天前
虚幻引擎_控制角色移动的三种方法
游戏引擎·虚幻
速冻鱼Kiel1 天前
GASP笔记01
笔记·ue5·游戏引擎·虚幻
孟无岐1 天前
【Laya】Animator2D 使用指南
typescript·游戏引擎·游戏程序·laya
速冻鱼Kiel1 天前
GASP笔记02
笔记·ue5·游戏引擎·虚幻
__water1 天前
RHK《Unity接入PicoSDK入门》
unity·游戏引擎·picosdk