Unity-协程

Unity-协程

基本使用方法

1
2
3
4
5
6
7
8
9
IEnumerator CoroutineFunc()
{
code...;
yield return ...;
code...;
}

StartCoroutine(CoroutineFunc);
StopCoroutine(CoroutineFunc);

函数会在yield return处停止,并把控制权转交给Unity,但会在下一帧中继续执行,如果想要提前结束的话则调用yield break

原理

Unity的协程是基于C#的迭代器实现的,但主要是C#2之后的迭代器

在C#1中,想要使用foreach关键字遍历某个对象,需要先让该对象实现IEnumerable的接口,同时还要实现一个迭代器类

在C#2中,引入了迭代块语句yield return来简化上述过程,程序员只需要实现1个返回参数类型为IEnumerator的函数即可获得C#1中用两个类实现的相同的效果,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static IEnumerator GetEnumerator()
{
int arr[] = {1, 2, 3};
for(int i = 0; i < 3; i++)
{
yield return arr[i];
}
}

// in main
foreach(int a in GetEnumerator())
{
// do something..
}

通过反编译可执行文件可以发现,我们编写的函数在编译器处理过后已经完全不再是一个函数了,特别是其中的yield return语句,和一般函数中的return语句完全不同,另外,函数中所有的局部变量都变成了生成迭代器类中的全局变量,可以通过反编译代码验证这一点。

假设C#源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Collections.Generic;

namespace net
{
class HelloWorld
{
static void Main(string[] args)
{
foreach(int i in GetEnumerator())
{

}
}

static IEnumerable<Int32> GetEnumerator()
{
for(int i = 0; i < 10; i++)
{
yield return i;
}
}
}
}

经过反编译后得到如下结果:

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace net
{
// Token: 0x02000002 RID: 2
internal class HelloWorld
{
// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
private static void Main(string[] args)
{
foreach (int i in HelloWorld.GetEnumerator())
{
}
}

// Token: 0x06000002 RID: 2 RVA: 0x0000209C File Offset: 0x0000029C
private static IEnumerable<int> GetEnumerator()
{
return new HelloWorld.<GetEnumerator>d__1(-2);
}

// Token: 0x02000003 RID: 3
[CompilerGenerated]
private sealed class <GetEnumerator>d__1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
{
// Token: 0x06000004 RID: 4 RVA: 0x000020AE File Offset: 0x000002AE
[DebuggerHidden]
public <GetEnumerator>d__1(int <>1__state)
{
this.<>1__state = <>1__state;
this.<>l__initialThreadId = Environment.CurrentManagedThreadId;
}

// Token: 0x06000005 RID: 5 RVA: 0x000020C9 File Offset: 0x000002C9
[DebuggerHidden]
void IDisposable.Dispose()
{
}

// Token: 0x06000006 RID: 6 RVA: 0x000020CC File Offset: 0x000002CC
bool IEnumerator.MoveNext()
{
int num = this.<>1__state;
if (num != 0)
{
if (num != 1)
{
return false;
}
this.<>1__state = -1;
int num2 = this.<i>5__1;
this.<i>5__1 = num2 + 1;
}
else
{
this.<>1__state = -1;
this.<i>5__1 = 0;
}
if (this.<i>5__1 >= 10)
{
return false;
}
this.<>2__current = this.<i>5__1;
this.<>1__state = 1;
return true;
}

// Token: 0x17000001 RID: 1
// (get) Token: 0x06000007 RID: 7 RVA: 0x0000213F File Offset: 0x0000033F
int IEnumerator<int>.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}

// Token: 0x06000008 RID: 8 RVA: 0x00002147 File Offset: 0x00000347
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}

// Token: 0x17000002 RID: 2
// (get) Token: 0x06000009 RID: 9 RVA: 0x0000214E File Offset: 0x0000034E
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}

// Token: 0x0600000A RID: 10 RVA: 0x0000215C File Offset: 0x0000035C
[DebuggerHidden]
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
HelloWorld.<GetEnumerator>d__1 result;
if (this.<>1__state == -2 && this.<>l__initialThreadId == Environment.CurrentManagedThreadId)
{
this.<>1__state = 0;
result = this;
}
else
{
result = new HelloWorld.<GetEnumerator>d__1(0);
}
return result;
}

// Token: 0x0600000B RID: 11 RVA: 0x00002193 File Offset: 0x00000393
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator()
{
return this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
}

// Token: 0x04000001 RID: 1
private int <>1__state;

// Token: 0x04000002 RID: 2
private int <>2__current;

// Token: 0x04000003 RID: 3
private int <>l__initialThreadId;

// Token: 0x04000004 RID: 4
private int <i>5__1;
}
}
}

可以发现,该函数被完全处理成了一个新的内部类,名称为<GetEnumerator>d__1, 其中,局部变量i变成了<i>5__1, 又增加了3个新的变量,其中<>1__state记录对象的当前状态,<>2__current记录下一个要返回的值,<>1__initialThreadId记录当前线程Id

同时,这个生成类变成了一个类似于状态机的东西,它将原来的局部变量作为状态机的上下文环境,将原本的代码逻辑按照复杂的规则转换成状态机中状态之间的切换,实现在MoveNext函数中,这样,在主函数完成对MoveNext的一次调用后,并不会丢失函数中声明的局部变量,同时也不会导致线程阻塞,有些类似于操作系统中的进程调度,这正是设计协程的良好框架!

但是,由于该方法需要将局部变量存储在堆中,如果频繁开启协程,则会申请大量空间,造成时间和空间上的巨大浪费,因此要谨慎使用协程,尽量不要在Update函数中每帧开启协程,并且大部分情况下这种代码逻辑是有问题的。

C++实现

虽然不知道微软使用了什么样的规则将包含yield的函数翻译成迭代器类,但是我们可以直接写一个迭代器类来实现相同的效果:

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include<iostream>
#include<memory>
#include<vector>
#include<conio.h>

using namespace std;

template <typename T>
using Ref = shared_ptr<T>;

static int id = 0;

int getId()
{
return id++;
}

class Enumerator
{
public:
Enumerator():state(-1), val(10), ID(getId()), timer(0), current(0)
{

}
bool MoveNext();

int state;

int ID;
int val;

int timer;

int current;
};

class Coroutine
{
public:
void Update();
void Add(Ref<Enumerator> Etr);

vector< Ref<Enumerator> > Etrs;
};

int main()
{
char ch;
Coroutine Crt;

while(1)
{
Crt.Update();
if(_kbhit())
{
if((ch = _getch()) == 'w')
{
Crt.Add(make_shared<Enumerator>());
}
}
}

return 0;
}

bool Enumerator::MoveNext()
{
switch(state)
{
case -1:
cout << ID << " start!\n";
state = 0;
case 0:
while(val >= 0)
{
cout << ID << ": " << val << endl;
val -= 1;

state = 2;
timer = 0;
return true;
}
state = 1;
case 1:
cout << ID << " end!\n";
return false;
case 2:
for(; timer < 10000;)
{
timer++;
return true;
}
state = 0;
return true;
}
return false;
}

void Coroutine::Add(Ref<Enumerator> Etr)
{
Etrs.push_back(Etr);
}

void Coroutine::Update()
{
for(int i = 0; i < Etrs.size(); i++)
{
Enumerator* Etr = Etrs[i].get();
if(!Etr->MoveNext())
{
Etrs.erase(Etrs.begin() + i);
i--;
}
}
}

在以上程序中,Enumerator为迭代器类,其MoveNext输出一个整数,并随着迭代次数不断减少。 Coroutine为管理协程的类,当按下w键时,程序向其中添加一个迭代器对象,Coroutine对象会在主线程中每帧更新所有迭代器对象,直到该对象迭代完成,将其弹出


Unity-协程
http://example.com/2023/01/10/Unity-协程/
作者
Chen Shuwen
发布于
2023年1月10日
许可协议