Unity协程详解
协程(Coroutine)底层如何实现
example
1 |
|
IEnumerator 迭代器
1 |
|
- 成员
- Current 当前值
- MoveNext() 获取下一个迭代对象
- Reset() 重置迭代器
- 返回值为IEnumrator的函数会被解析成迭代器函数(ILSpy或dotPeek可以看编译后的代码)
- 这类函数可以看作是加了书签(yield)的书,被yield修饰的return不意味着函数生命周期结束而是在此处被挂起
IEnumerable 迭代对象
1 |
|
- 实现IEnumable接口的类被作为可迭代对象
- 可以被按照顺序游览的景点(IEnumerable)配备一个携带地图的导游(IEnumerator)
MonoBehaiour与协程的关系
- Unity的MonoBehaiour是单线程的,事件驱动系统实际上是类似时间片轮转的处理逻辑
- 协程在MonoBehaiour中的生命周期
1
2
3
4
5
6
7
8
9
10
11FixedUpdate
yield WaitForFixedUpdate
↓
Update
yield null
yield WaitForSeconds
yield WWW
yield StartCoroutine
LateUpdate
↓
yield WaitForEndOfFrame - StartCoroutine方法用于将迭代器函数注册到MonoBehaviour的协程管理器中
- WaitForSeconds相当于Unity自己维护的计时器,第一次调用DelayedExecution这个函数会获得WaitForSeconds(2)的返回值,之后会标记这个函数暂且休眠,直到计时结束
栈帧和状态机
- 迭代器状态机
- 在 C# 中,使用 yield return 语句返回迭代器时,编译器会自动生成一个状态机,该状态机将迭代器函数转换为一个带有状态的对象。这个状态机被称为迭代器状态机(Iterator State Machine)。
- 迭代器状态机是一个类,它实现了 IEnumerator 接口。这个类包含一个或多个字段,用于保存迭代器函数的状态信息,例如当前执行的位置、当前值等。迭代器状态机还实现了 IEnumerator 接口中的 MoveNext()、Current 和 Reset() 方法,以便可以使用 foreach 循环或者显式调用 IEnumerator 接口的方法来访问迭代器函数的返回值。
- 当迭代器函数使用 yield return 语句返回一个值时,状态机将保存该值以及当前执行的位置,并将控制权返回给调用方。当迭代器函数再次被调用时,状态机将从上一次保存的位置继续执行,并将保存的值返回给调用方。这个过程将重复,直到迭代器函数执行完毕或者使用 yield break 语句退出迭代器。
- 在生成的状态机中,局部变量和参数的值将被保存在状态对象中,以便在协程恢复时可以继续使用。因此,即使在协程挂起期间,局部参数的值也会被保存在状态对象中,以便在协程恢复时可以继续使用。
- 栈帧
栈帧(Stack Frame)是指在函数调用期间,为了保存局部变量、参数、返回地址和其他相关信息而在栈上创建的一块内存区域。每当一个函数被调用时,都会在栈上创建一个新的栈帧,用于保存该函数的局部变量和其他信息。当函数返回时,该栈帧将被销毁,栈指针将恢复到上一个栈帧的位置,以便继续执行上一个函数。栈帧通常包含以下信息:
- 返回地址:指向调用该函数的指令的地址,以便在函数返回后继续执行调用方的代码。
- 保存的寄存器:保存在函数调用期间需要保护的寄存器的值,以便在函数返回时恢复这些寄存器的值。
- 函数参数:保存在函数调用期间传递给函数的参数的值。
- 局部变量:保存在函数调用期间在函数内部定义的局部变量的值。
- 栈指针:指向当前栈帧的栈顶位置,用于分配和释放栈上的内存空间。
协程管理器
unity会维护一个协程管理器(单例对象),协程管理器使用了一种称为“协作式多任务”的技术,它允许多个任务在同一时间共享 CPU 资源,而不是像线程那样在同一时间竞争 CPU 资源。协程管理器会在每一帧检查所有的协程状态,并根据需要恢复协程的执行。这种方式可以避免线程竞争带来的问题,同时也可以减少 CPU 的占用率,提高游戏的性能。
反编译&解析
- C#
1
2
3
4
5
6
7
8
9
10
11public 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");
}
} - 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
80public 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协程详解/