Unity协程的内部实现分析并模拟实现

2016/5/18 Unity游戏开发

Unity协程的是如何实现的?

# 引言

使用Unity协程我们可以非常方便的实现某些延迟逻辑,协程使用IEnumerator + yield语句来实现函数的分段执行。协程不是线程,协程和主逻辑在同一个线程中执行,它使用yield语句将代码段分散在不同帧执行,是一种异步执行代码的方式,但是代码看起来感觉又是同步执行的。

# yield return与IEnumerator

C#中使用yield return语句来实现自定义的集合类型的IEnumerable和IEnumerator模式, 而不需要自己实现IEnumerator接口。当代码执行到yield return expresson,表达式的值返回并可通过IEnumerator.Current访问,同时当前代码运行位置及状态保存了下来,下次执行会从当前位置状态开始执行。IEnumerator.MoveNext()执行下一段代码。通过保存IEnumerator实例就可以控制什么时候执行下一段代码。

IEnumerator Numbers()
{
    for (int i = 0; i < 3; i++)
    {
        yield return i;
    }
    yield return new WaitForSeconds(5);
}

void Start() {
    IEnumerator enumerator = Numbers();
    Debug.Log(enumerator.Current); // Null
    
    // 遍历下一个元素
    enumerator.MoveNext(); 
    Debug.Log(enumerator.Current); // 0
    Debug.Log(enumerator.Current); // 0

    enumerator.MoveNext();
    enumerator.MoveNext();  
    Debug.Log(enumerator.Current); // 2

    enumerator.MoveNext(); 
    // 这里不会真正的WaitForSeconds
    Debug.Log(enumerator.Current); // UnityEngine.WaitForSeconds
}
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

# 模拟协程

上面代码可以看到,yield return语句把函数分段,而什么时候执行这些函数断是通过调用IEnumerator.MoveNext()来决定的。我们保存IEnumerator实例,将MoveNext()放到MonoBehaviour.Update()函数中执行,则这些片段就分散在每帧中执行了。

IEnumerator enumerator;

void StartRoutine(IEnumerator coroutine)
{
    enumerator = Routine();
}

IEnumerator Routine()
{
    Debug.Log("step1.");
    yield return 1;
    Debug.Log("step2.");
    yield return 2;
    Debug.Log("step3.");
    // 这里实际没有等待1秒, 现在表达式的值没有用到
    yield return new WaitForSeconds(1);
    Debug.Log("step4.");
}

void Start()
{
    StartRoutine(Routine());
}
void Update()
{
    if (enumerator != null)
    {
        if (!enumerator.MoveNext())
        {
            enumerator = null;
        }
    }
}
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

Routine代码总共分了4帧执行完毕,到目前为止我们还没有把yield return表达式的值没有运用起来,比如WaitForSeconds没有真正等待。我们可以通过Current访问yield return表达式的值来实现。

void Update()
{
    if (enumerator != null)
    {
        var waiting = enumerator.Current as WaitForSeconds;
        if (waiting != null && !waiting.isDone) // 伪代码
        {
            // 保持等待,不调用MoveNext()
        }
        else if (!enumerator.MoveNext())
        {
            enumerator = null;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 完整模拟代码

上面的代码简单的描述了协程实现原理,实际的协程实现远比这复杂的多,需要实现嵌套协程,异步操作等等。

/// <summary>
/// 协程实现伪代码
/// </summary>
public class CoroutinePseudoRuntime
{
    private List<IEnumerator> coroutines = new List<IEnumerator>();

    private List<IEnumerator> needRemoveCoroutines = new List<IEnumerator>();

    public void RunNormalCoroutine()
    {
        for (int i = 0; i < coroutines.Count; i++)
        {
            if (!ExecuteNormalCoroutine(coroutines[i]))
            {
                // 协程已经执行完毕,需要删除
                needRemoveCoroutines.Add(coroutines[i]);
            }
        }

        GCCoroutines();
    }

    public void RunParticularCoroutine<T>() where T : YieldInstruction
    {
        for (int i = 0; i < coroutines.Count; i++)
        {
            if (!ExecuteParticularCoroutine<T>(coroutines[i]))
            {
                needRemoveCoroutines.Add(coroutines[i]);
            }
        }

        GCCoroutines();
    }

    /// <summary>
    /// 递归执行特定yield instruction
    /// </summary>
    /// <typeparam name="T">特定yield instruction</typeparam>
    /// <param name="coroutine">协程</param>
    /// <returns>协程是否在执行中</returns>
    bool ExecuteParticularCoroutine<T>(IEnumerator coroutine) where T : YieldInstruction
    {
        IEnumerator child = coroutine.Current as IEnumerator;
        if (child != null)
        {
            // 在子协程中执行特定yield instruction
            bool childResult = ExecuteParticularCoroutine<T>(child);
            if (childResult == false)
            {
                // 子协程执行完毕 继续执行父协程
                return coroutine.MoveNext();
            }

            return childResult;
        }

        T current = coroutine.Current as T;
        if (current != null)
        {
            return coroutine.MoveNext();
        }

        // 没有找到特定yield instruction
        return true;
    }

    bool ExecuteNormalCoroutine(IEnumerator coroutine)
    {
        var waitForEndOfFrame = coroutine.Current as WaitForEndOfFrame;
        if (waitForEndOfFrame != null)
        {
            // 等待WaitForEndOfFrame执行完毕
            // 在LateUpdate后执行
            return true;
        }

        var waitForFixedUpdate = coroutine.Current as WaitForFixedUpdate;
        if (waitForFixedUpdate != null)
        {
            // 等待WaitForFixedUpdate执行完毕
            // 在FixedUpdate后执行
            return true;
        }

        var waitForSeconds = coroutine.Current as WaitForSeconds;
        if (waitForSeconds != null)
        {
            // 等待WaitForSeconds  isDone是伪代码
            //if (waitForSeconds.isDone)
            //{
            //    return true;
            //}
            return coroutine.MoveNext();
        }

        var www = coroutine.Current as WWW;
        if (www != null)
        {
            // 等待www完成
            if (www.isDone)
            {
                return coroutine.MoveNext();
            }
            return true;
        }

        var asyncOperation = coroutine.Current as AsyncOperation;
        if (asyncOperation != null)
        {
            // 等待异步操作完成
            if (asyncOperation.isDone)
            {
                return coroutine.MoveNext();
            }
            return true;
        }

        var childCoroutine = coroutine.Current as IEnumerator;
        if (childCoroutine != null)
        {
            // 执行子协程
            bool waiting = ExecuteNormalCoroutine(childCoroutine);
            if (waiting)
            {
                return true;
            }
            return coroutine.MoveNext();
        }

        // 这里省略了其他的YieldInstruction


        // null 及 其他
        return coroutine.MoveNext();
    }
 
    void GCCoroutines()
    {
        for (int i = 0; i < needRemoveCoroutines.Count; i++)
        {
            coroutines.Remove(needRemoveCoroutines[i]);
        }
    }

    /// <summary>
    /// 开启一个协程
    /// </summary>
    /// <param name="coroutine"></param>
    public void StartCoroutine(IEnumerator coroutine)
    {
        coroutines.Add(coroutine);
    }

    /// <summary>
    /// 停止一个协程
    /// </summary>
    /// <param name="coroutine"></param>
    /// <returns></returns>
    public bool StopCoroutine(IEnumerator coroutine)
    {
        return coroutines.Remove(coroutine);
    }
}

public class CoroutinePseudoBehaviour : MonoBehaviour
{
    void Start()
    {
        coroutineRuntime.StartCoroutine(CoroutineTest());
    }
 
    IEnumerator WaitUntilUnscaled(float until)
    {
        while (Time.unscaledTime < until)
        {
            yield return null;
        }
    }

    IEnumerator WaitForFrame(int frameCount)
    {
        for (int i = 0; i < frameCount; i++)
        {
            yield return new WaitForEndOfFrame();
        }
    }

    IEnumerator WaitUntilThenFrame(float until, int frame)
    {
        yield return WaitUntilUnscaled(until);
        Debug.Log("after wait until...");
        yield return WaitForFrame(frame);
        Debug.LogFormat("after wait {0} frames...", frame);
    }

    IEnumerator CoroutineTest()
    {
        Debug.Log("start...");
        yield return new WaitForFixedUpdate();
        Debug.Log("after Fixed update...");
        yield return new WaitForEndOfFrame();
        Debug.Log("after late update...");
        yield return (WaitUntilUnscaled(Time.unscaledTime + 1));
        Debug.Log("1 seconds elapsed...");

        yield return WaitUntilThenFrame(Time.unscaledTime + 1, 10);
        Debug.Log("after 1 seconds and 10 frames...");

        WWW www = new WWW("http://docs.unity3d.com/ScriptReference/CustomYieldInstruction.html");
        yield return www;
        Debug.Log(www.text);
    }

    CoroutinePseudoRuntime coroutineRuntime = new CoroutinePseudoRuntime();

    void Update()
    {
        // 一般的协程在Update之后 LateUpdate之前执行
        coroutineRuntime.RunNormalCoroutine();
    }

    void LateUpdate()
    {
        // Unity中WaitForEndOfFrame在 LateUpdate之后执行 这里模拟在MonoBehavior.LateUpdate中
        coroutineRuntime.RunParticularCoroutine<WaitForEndOfFrame>();
    }

    void FixedUpdate()
    {
        // Unity中 WaitForFixedUpdate在FixedUpdate之后执行 这里模拟在MonoBehavior.FixedUpdate中
        coroutineRuntime.RunParticularCoroutine<WaitForFixedUpdate>();
    }
}
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235

# 结语

上面简单分析了yield return语句和IEnumerator接口这一实现协程的C#基础概念,并利用他们模拟实现了一个协程运行时库,可以清楚的看到协程不外乎就是把代码分散到不同时间执行,它跟线程是有本质的区别的。

Last Updated: 2022/1/8 04:00:18