Unity协程详解

协程(Coroutine)底层如何实现

example

1
2
3
4
5
6
7
8
9
10
11
void Start()
{
StartCoroutine(DelayedExecution());
}

IEnumerator DelayedExecution()
{
Debug.Log("开始延迟两秒");
yield return new WaitForSeconds(2); // 延迟2秒
Debug.Log("延迟两秒结束");
}

IEnumerator 迭代器

1
2
3
4
5
6
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
  1. 成员
  • Current 当前值
  • MoveNext() 获取下一个迭代对象
  • Reset() 重置迭代器
  1. 返回值为IEnumrator的函数会被解析成迭代器函数(ILSpy或dotPeek可以看编译后的代码)
  • 这类函数可以看作是加了书签(yield)的书,被yield修饰的return不意味着函数生命周期结束而是在此处被挂起

IEnumerable 迭代对象

1
2
3
4
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
  1. 实现IEnumable接口的类被作为可迭代对象
  2. 可以被按照顺序游览的景点(IEnumerable)配备一个携带地图的导游(IEnumerator)

MonoBehaiour与协程的关系

  1. Unity的MonoBehaiour是单线程的,事件驱动系统实际上是类似时间片轮转的处理逻辑
  2. 协程在MonoBehaiour中的生命周期
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    FixedUpdate
    yield WaitForFixedUpdate

    Update
    yield null
    yield WaitForSeconds
    yield WWW
    yield StartCoroutine
    LateUpdate

    yield WaitForEndOfFrame
  3. StartCoroutine方法用于将迭代器函数注册到MonoBehaviour的协程管理器中
  4. WaitForSeconds相当于Unity自己维护的计时器,第一次调用DelayedExecution这个函数会获得WaitForSeconds(2)的返回值,之后会标记这个函数暂且休眠,直到计时结束

栈帧和状态机

  1. 迭代器状态机
  • 在 C# 中,使用 yield return 语句返回迭代器时,编译器会自动生成一个状态机,该状态机将迭代器函数转换为一个带有状态的对象。这个状态机被称为迭代器状态机(Iterator State Machine)。
  • 迭代器状态机是一个类,它实现了 IEnumerator 接口。这个类包含一个或多个字段,用于保存迭代器函数的状态信息,例如当前执行的位置、当前值等。迭代器状态机还实现了 IEnumerator 接口中的 MoveNext()、Current 和 Reset() 方法,以便可以使用 foreach 循环或者显式调用 IEnumerator 接口的方法来访问迭代器函数的返回值。
  • 当迭代器函数使用 yield return 语句返回一个值时,状态机将保存该值以及当前执行的位置,并将控制权返回给调用方。当迭代器函数再次被调用时,状态机将从上一次保存的位置继续执行,并将保存的值返回给调用方。这个过程将重复,直到迭代器函数执行完毕或者使用 yield break 语句退出迭代器。
  • 在生成的状态机中,局部变量和参数的值将被保存在状态对象中,以便在协程恢复时可以继续使用。因此,即使在协程挂起期间,局部参数的值也会被保存在状态对象中,以便在协程恢复时可以继续使用。
  1. 栈帧
    栈帧(Stack Frame)是指在函数调用期间,为了保存局部变量、参数、返回地址和其他相关信息而在栈上创建的一块内存区域。每当一个函数被调用时,都会在栈上创建一个新的栈帧,用于保存该函数的局部变量和其他信息。当函数返回时,该栈帧将被销毁,栈指针将恢复到上一个栈帧的位置,以便继续执行上一个函数。栈帧通常包含以下信息:
  • 返回地址:指向调用该函数的指令的地址,以便在函数返回后继续执行调用方的代码。
  • 保存的寄存器:保存在函数调用期间需要保护的寄存器的值,以便在函数返回时恢复这些寄存器的值。
  • 函数参数:保存在函数调用期间传递给函数的参数的值。
  • 局部变量:保存在函数调用期间在函数内部定义的局部变量的值。
  • 栈指针:指向当前栈帧的栈顶位置,用于分配和释放栈上的内存空间。

协程管理器

unity会维护一个协程管理器(单例对象),协程管理器使用了一种称为“协作式多任务”的技术,它允许多个任务在同一时间共享 CPU 资源,而不是像线程那样在同一时间竞争 CPU 资源。协程管理器会在每一帧检查所有的协程状态,并根据需要恢复协程的执行。这种方式可以避免线程竞争带来的问题,同时也可以减少 CPU 的占用率,提高游戏的性能。

反编译&解析

  1. C#
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Coroutine2
    {
    static IEnumerator Coroutine()
    {
    Console.WriteLine("Coroutine started");
    yield return null;
    Console.WriteLine("Coroutine resumed");
    yield return new WaitForSeconds(1);
    Console.WriteLine("Coroutine finished");
    }
    }
  2. CIL
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    public class Coroutine2
    {
    [IteratorStateMachine(typeof (Coroutine2.<Coroutine>d__0))]
    private static IEnumerator Coroutine()
    {
    return (IEnumerator) new Coroutine2.<Coroutine>d__0(0);
    }

    public Coroutine2()
    {
    base..ctor();
    }

    [CompilerGenerated]
    private sealed class <Coroutine>d__0 : IEnumerator<object>, IEnumerator, IDisposable
    {
    private int <>1__state;
    private object <>2__current;

    [DebuggerHidden]
    public <Coroutine>d__0(int _param1)
    {
    base..ctor();
    this.<>1__state = _param1;
    }

    [DebuggerHidden]
    void IDisposable.Dispose()
    {
    }

    bool IEnumerator.MoveNext()
    {
    switch (this.<>1__state)
    {
    case 0:
    this.<>1__state = -1;
    Console.WriteLine("Coroutine started");
    this.<>2__current = (object) null;
    this.<>1__state = 1;
    return true;
    case 1:
    this.<>1__state = -1;
    Console.WriteLine("Coroutine resumed");
    this.<>2__current = (object) new WaitForSeconds(1f);
    this.<>1__state = 2;
    return true;
    case 2:
    this.<>1__state = -1;
    Console.WriteLine("Coroutine finished");
    return false;
    default:
    return false;
    }
    }

    object IEnumerator<object>.Current
    {
    [DebuggerHidden] get
    {
    return this.<>2__current;
    }
    }

    [DebuggerHidden]
    void IEnumerator.Reset()
    {
    throw new NotSupportedException();
    }

    object IEnumerator.Current
    {
    [DebuggerHidden] get
    {
    return this.<>2__current;
    }
    }
    }
    }

  • 可以看到编译器将迭代器函数转化为一个类(迭代器状态机) d__0
  • 每个yield成为MoveNext()方法中switch中的一个分支,每次调用更新state和current
  • unity通过协程管理器再恰当的时机调用迭代器函数,并更新状态直至协程完成,从维护的队列移除

Unity协程详解
https://baifabaiquan.cn/2023/09/04/unity协程详解/
作者
白发败犬
发布于
2023年9月4日
许可协议