Unity協程(Coroutine)原理深入剖析再續
By D.S.Qiu
尊重他人的勞動,支持原創,轉載請注明出處:http.dsqiu.iteye.com
前面已經介紹過對協程(Coroutine)的認識和理解,主要講到了Unity引擎在執行協程(Coroutine)的原理(Unity協程(Coroutine)原理深入剖析)和對協程(Coroutine)狀態的控制(Unity協程(Coroutine)管理類——TaskManager工具分享),到這使用Coroutine的疑問就沒有了,但是D.S.Qiu還是有點沒嚼爛,所以覺得很有必要再續。
本文主要分為三部分:
1)yield return, IEnumerator 和 Unity StartCoroutine 的關系和理解
2)Cortoutine 擴展——Extending Coroutines: Return Values and Error Handling
3)Cortountine Locking
總之,引用③的一句話:Coroutines – More than you want to know.
1)yield return, IEnumerator 和 Unity StartCoroutine 的關系和理解
yield 和 IEnumerator都是C#的東西,前者是一個關鍵字,后者是枚舉類的接口。對於IEnumerator 只引用②對 IEnumerable與IEnumerator區別 的論述:
先貼出 IEnumerable 和 IEnumerator的定義:
- public interface IEnumerable
- {
- IEnumerator GetEnumerator();
- }
- public interface IEnumerator
- {
- bool MoveNext();
- void Reset();
- Object Current { get; }
- }
IEnumerable和IEnumerator有什么區別?這是一個很讓人困惑的問題(在很多forum里都看到有人在問這個問題)。研究了半天,得到以下幾點認識:
1、一個Collection要支持foreach方式的遍歷,必須實現IEnumerable接口(亦即,必須以某種方式返回IEnumerator object)。
2、IEnumerator object具體實現了iterator(通過MoveNext(),Reset(),Current)。
3、從這兩個接口的用詞選擇上,也可以看出其不同:IEnumerable是一個聲明式的接口,聲明實現該接口的class是“可枚舉(enumerable)”的,但並沒有說明如何實現枚舉器(iterator);IEnumerator是一個實現式的接口,IEnumerator object就是一個iterator。
4、IEnumerable和IEnumerator通過IEnumerable的GetEnumerator()方法建立了連接,client可以通過IEnumerable的GetEnumerator()得到IEnumerator object,在這個意義上,將GetEnumerator()看作IEnumerator object的factory method也未嘗不可。
IEnumerator 是所有枚舉數的基接口。
枚舉數只允許讀取集合中的數據。枚舉數無法用於修改基礎集合。
最初,枚舉數被定位於集合中第一個元素的前面。Reset 也將枚舉數返回到此位置。在此位置,調用 Current 會引發異常。因此,在讀取 Current 的值之前,必須調用 MoveNext 將枚舉數提前到集合的第一個元素。
在調用 MoveNext 或 Reset 之前,Current 返回同一對象。MoveNext 將 Current 設置為下一個元素。
在傳遞到集合的末尾之后,枚舉數放在集合中最后一個元素后面,且調用 MoveNext 會返回 false。如果最后一次調用 MoveNext 返回 false,則調用 Current 會引發異常。若要再次將 Current 設置為集合的第一個元素,可以調用 Reset,然后再調用 MoveNext。
只要集合保持不變,枚舉數就將保持有效。如果對集合進行了更改(例如添加、修改或刪除元素),則該枚舉數將失效且不可恢復,並且下一次對 MoveNext 或 Reset 的調用將引發 InvalidOperationException。如果在 MoveNext 和 Current 之間修改集合,那么即使枚舉數已經無效,Current 也將返回它所設置成的元素。
枚舉數沒有對集合的獨占訪問權;因此,枚舉一個集合在本質上不是一個線程安全的過程。甚至在對集合進行同步處理時,其他線程仍可以修改該集合,這會導致枚舉數引發異常。若要在枚舉過程中保證線程安全,可以在整個枚舉過程中鎖定集合,或者捕捉由於其他線程進行的更改而引發的異常。
Yield關鍵字
在迭代器塊中用於向枚舉數對象提供值或發出迭代結束信號。它的形式為下列之一⑥:
yield return <expression_r>;
yield break;
備注 :
計算表達式並以枚舉數對象值的形式返回;expression_r 必須可以隱式轉換為迭代器的 yield 類型。
yield 語句只能出現在 iterator 塊中,該塊可用作方法、運算符或訪問器的體。這類方法、運算符或訪問器的體受以下約束的控制:
不允許不安全塊。
方法、運算符或訪問器的參數不能是 ref 或 out。
yield 語句不能出現在匿名方法中。
當和 expression_r 一起使用時,yield return 語句不能出現在 catch 塊中或含有一個或多個 catch 子句的 try 塊中。
yield return 提供了迭代器一個比較重要的功能,即取到一個數據后馬上返回該數據,不需要全部數據裝入數列完畢,這樣有效提高了遍歷效率。
Unity StartCoroutine
Unity使用 StartCoroutine(routine: IEnumerator): Coroutine 啟動協程,參數必須是 IEnumerator 對象。那么Unity在背后做什么神奇的處理呢?
StartCoroutine函數的參數我一般都是通過傳入一個返回值為 IEnumerator的函數得到的:
- IEnumerator WaitAndPrint(float waitTime) {
- yield return new WaitForSeconds(waitTime);
- print("WaitAndPrint " + Time.time);
- }
在函數內使用前面介紹 yield 關鍵字返回 IEnumerator 對象,Unity 中實現了 YieldInstruction 作為 yield 返回的基類,有 Cortoutine, WaitForSecondes, WaitForEndOfFrame, WaitForFixedUpdate, WWW 幾個子類實現。StartCoroutine 將 傳入的 IEnumerator 封裝為 Coroutine 返回,引擎會對 Corountines 存儲和檢查 IEnumerator 的 Current值。
③枚舉了 WWW ,WaitForSeconds , null 和 WaitForEndOfFrame 檢查 Current值在MonoBebaviour生存周期的時間(沒有WaitForFixedUpdate ,D.S.Qiu猜測是其作者成文是Unity引擎還沒有提供這個實現):
WWW - after Updates happen for all game objects; check the isDone flag. If true, call the IEnumerator's MoveNext() function;
WaitForSeconds - after Updates happen for all game objects; check if the time has elapsed, if it has, call MoveNext();
null or some unknown value - after Updates happen for all game objects; Call MoveNext();
WaitForEndOfFrame - after Render happens for all cameras; Call MoveNext().
如果最后一個 yield return 的 IEnumerator 已經迭代到最后一個是,MoveNext 就會 返回 false 。這時,Unity就會將這個 IEnumerator 從 cortoutines list 中移除。
所以很容易一個出現的誤解:協程 Coroutines 並不是並行的,它和你的其他代碼都運行在同一個線程中,所以才會在Update 和 Coroutine中使用 同一個值時才會變得線程安全。這就是Unity對線程安全的解決策略——直接不使用線程,最近Unity 5 將要發布說的很熱,看到就有完全多線程的支持,不知道是怎么實現的,從技術的角度,還是很期待的哈。
總結下: 在協程方法中使用 yield return 其實就是為了返回 IEnumerator對象,只有當這個對象的 MoveNext() 返回 false 時,即該 IEnumertator 的 Current 已經迭代到最后一個元素了,才會執行 yield return 后面的語句。也就是說, yield return 被會“翻譯”為一個 IEnmerator 對象,要想深入了解這方面的更多細節,可以猛擊⑤查看。
根據⑤ C# in depth 的理解——C# 編譯器會生成一個 IEnumerator 對象,這個對象實現的 MoveNext() 包含函數內所有 yield return 的處理,這里僅附上一個例子:
- using System;
- using System.Collections;
- class Test
- {
- static IEnumerator GetCounter()
- {
- for (int count = 0; count < 10; count++)
- {
- yield return count;
- }
- }
- }
C#編譯器對應生成:
- internal class Test
- {
- // Note how this doesn't execute any of our original code
- private static IEnumerator GetCounter()
- {
- return new <GetCounter>d__0(0);
- }
- // Nested type automatically created by the compiler to implement the iterator
- [CompilerGenerated]
- private sealed class <GetCounter>d__0 : IEnumerator<object>, IEnumerator, IDisposable
- {
- // Fields: there'll always be a "state" and "current", but the "count"
- // comes from the local variable in our iterator block.
- private int <>1__state;
- private object <>2__current;
- public int <count>5__1;
- [DebuggerHidden]
- public <GetCounter>d__0(int <>1__state)
- {
- this.<>1__state = <>1__state;
- }
- // Almost all of the real work happens here
- private bool MoveNext()
- {
- switch (this.<>1__state)
- {
- case 0:
- this.<>1__state = -1;
- this.<count>5__1 = 0;
- while (this.<count>5__1 < 10) //這里針對循環處理
- {
- this.<>2__current = this.<count>5__1;
- this.<>1__state = 1;
- return true;
- Label_004B:
- this.<>1__state = -1;
- this.<count>5__1++;
- }
- break;
- case 1:
- goto Label_004B;
- }
- return false;
- }
- [DebuggerHidden]
- void IEnumerator.Reset()
- {
- throw new NotSupportedException();
- }
- void IDisposable.Dispose()
- {
- }
- object IEnumerator<object>.Current
- {
- [DebuggerHidden]
- get
- {
- return this.<>2__current;
- }
- }
- object IEnumerator.Current
- {
- [DebuggerHidden]
- get
- {
- return this.<>2__current;
- }
- }
- }
- }
從上面的C#實現可以知道:函數內有多少個 yield return 在對應的 MoveNext() 就會返回多少次 true (不包含嵌套)。另外非常重要的一點的是:同一個函數內的其他代碼(不是 yield return 語句)會被移到 MoveNext 中去,也就是說,每次 MoveNext 都會順帶執行同一個函數中 yield return 之前,之后 和兩個 yield return 之間的代碼。
對於Unity 引擎的 YieldInstruction 實現,其實就可以看着一個 函數體,這個函數體每幀會實現去 check MoveNext 是否返回 false 。 例如:
- yield retrun new WaitForSeconds(2f);
上面這行代碼的偽代碼實現:
- private float elapsedTime;
- private float time;
- private void MoveNext()
- {
- elapesedTime += Time.deltaTime;
- if(time <= elapsedTime)
- return false;
- else return true;
- }
增補於: 2014年04月22日 8:00
2)Cortoutine 擴展——Extending Coroutines: Return Values and Error Handling
不知道你們調用 StartCortoutine 的時候有沒有注意到 StartCortoutine 返回了 YieldInstruction 的子類 Cortoutine 對象,這個返回除了嵌套使用 StartCortoutine 在 yiled retrun StartCortoutine 有用到,其他情況機會就沒有考慮它的存在,反正D.S.Qiu是這樣的,一直認為物“極”所用,所以每次調用 StartCortoutine 都很糾結,好吧,有點強迫症。
Unity引擎講 StartCoroutine 傳入的參數 IEnumerator 封裝為一個 Coroutine 對象中,而 Coroutine 對象其實也是 IEnumerator 枚舉對象。yield return 的 IEnumerator 對象都存儲在這個 Coroutine 中,只有當上一個yield return 的 IEnumerator 迭代完成,才會運行下一個。這個在猜測下Unity底層對Cortountine 的統一管理(也就是上面說的檢查 Current 值):Unity底層應該有一個 正在運行的 Cortoutine 的 list 然后在每幀的不同時間去 Check。
還是回歸到主題,上面介紹 yield 關鍵字有說不允許不安全塊,也就是說不能出現在 try catch 塊中,就不能在 yield return 執行是進行錯誤檢查。③利用 StartCortoutine 返回值 Cortoutine 得到了當前的 Current 值和進行錯誤捕獲處理。
先定義封裝包裹返回值和錯誤信息的類:
- public class Coroutine<T>{
- public T Value {
- get{
- if(e != null){
- throw e;
- }
- return returnVal;
- }
- }
- private T returnVal; //當前迭代器的Current 值
- private Exception e; //拋出的錯誤信息
- public Coroutine coroutine;
- public IEnumerator InternalRoutine(IEnumerator coroutine){
- //先省略這部分的處理
- }
- }
InteralRoutine是對返回 Current 值和拋出的異常信息(如果有的話):
- public IEnumerator InternalRoutine(IEnumerator coroutine){
- while(true){
- try{
- if(!coroutine.MoveNext()){
- yield break;
- }
- }
- catch(Exception e){
- this.e = e;
- yield break;
- }
- object yielded = coroutine.Current;
- if(yielded != null && yielded.GetType() == typeof(T)){
- returnVal = (T)yielded;
- yield break;
- }
- else{
- yield return coroutine.Current;
- }
- }
下面為這個類擴展MonoBehavior:
- public static class MonoBehaviorExt{
- public static Coroutine<T> StartCoroutine<T>(this MonoBehaviour obj, IEnumerator coroutine){
- Coroutine<T> coroutineObject = new Coroutine<T>();
- coroutineObject.coroutine = obj.StartCoroutine(coroutineObject.InternalRoutine(coroutine));
- return coroutineObject;
- }
- }
最后給出一個 Example:
- IEnumerator Start () {
- var routine = StartCoroutine<int>(TestNewRoutine()); //Start our new routine
- yield return routine.coroutine; // wait as we normally can
- Debug.Log(routine.Value); // print the result now that it is finished.
- }
- IEnumerator TestNewRoutine(){
- yield return null;
- yield return new WaitForSeconds(2f);
- yield return 10;
- yield return 5;
- }
最后輸出是10,因為Cortoutine<T> 遇到滿足條件的 T 類型就 執行 yield break;就不執行 yield return 5; 這條語句了。
如果將中 yield break; 語句去掉的話,最后輸出的是 5 而不是10。
- if(yielded != null && yielded.GetType() == typeof(T)){
- returnVal = (T)yielded;
- yield break;
- }
其實就是Unity引擎每幀去 check yield return 后面的表達式,如果滿足就繼續向下執行。
下面在測試一個例子:連續兩次調用 yield return coroutine;
- private Coroutine routine1;
- void Start ()
- {
- routine1 = StartCoroutine(TestCoroutineExtention1()); //Start our new routine
- StartCoroutine(TestCortoutine());
- }
- IEnumerator TestCoroutineExtention1()
- {
- yield return new WaitForSeconds(1);
- yield return 10;
- Debug.Log("Run 10!");
- yield return new WaitForSeconds(5);
- yield return 5;
- Debug.Log("Run 5!");
- }
- IEnumerator TestCortoutine()
- {
- //wwwState = true;
- yield return routine1; // wait as we normally can
- Debug.Log(" routine1");
- yield return routine1; // wait as we normally can
- Debug.Log(" routine2");
- }
測試運行會發現只會輸出:
Run 10!
Run 5!
routine1
總結下: yield return expression 只有表達式完全執行結束才會繼續執行后面的代碼,連續兩次執行 yield return StartCortoutine() 的返回值是不會滿足的,說明 yield return 有區分開始和結束的兩種狀態。
3)Cortoutine Locking
雖然Cortoutine不是多線程機制,但仍會“並發”問題——同時多次調用 StartCortoutine ,當然通過Unity提供的api也能得到解決方案,每次StartCoroutine 之前先調用 StopCortoutine 方法停止,但這利用的是反射,顯然效率不好。④對③的方案進行了擴展提供了 Cortoutine Locking 的支持,使用字符串(方法名)來標記同一個 Coroutine 方法,對於同一個方法如果等待時間超過 timeout 就會終止前面一個 Coroutine 方法,下面直接貼出代碼:
- using UnityEngine;
- using System;
- using System.Collections;
- using System.Collections.Generic;
- /// <summary>
- /// Extending MonoBehaviour to add some extra functionality
- /// Exception handling from: http://twistedoakstudios.com/blog/Post83_coroutines-more-than-you-want-to-know
- ///
- /// 2013 Tim Tregubov
- /// </summary>
- public class TTMonoBehaviour : MonoBehaviour
- {
- private LockQueue LockedCoroutineQueue { get; set; }
- /// <summary>
- /// Coroutine with return value AND exception handling on the return value.
- /// </summary>
- public Coroutine<T> StartCoroutine<T>(IEnumerator coroutine)
- {
- Coroutine<T> coroutineObj = new Coroutine<T>();
- coroutineObj.coroutine = base.StartCoroutine(coroutineObj.InternalRoutine(coroutine));
- return coroutineObj;
- }
- /// <summary>
- /// Lockable coroutine. Can either wait for a previous coroutine to finish or a timeout or just bail if previous one isn't done.
- /// Caution: the default timeout is 10 seconds. Coroutines that timeout just drop so if its essential increase this timeout.
- /// Set waitTime to 0 for no wait
- /// </summary>
- public Coroutine<T> StartCoroutine<T>(IEnumerator coroutine, string lockID, float waitTime = 10f)
- {
- if (LockedCoroutineQueue == null) LockedCoroutineQueue = new LockQueue();
- Coroutine<T> coroutineObj = new Coroutine<T>(lockID, waitTime, LockedCoroutineQueue);
- coroutineObj.coroutine = base.StartCoroutine(coroutineObj.InternalRoutine(coroutine));
- return coroutineObj;
- }
- /// <summary>
- /// Coroutine with return value AND exception handling AND lockable
- /// </summary>
- public class Coroutine<T>
- {
- private T returnVal;
- private Exception e;
- private string lockID;
- private float waitTime;
- private LockQueue lockedCoroutines; //reference to objects lockdict
- private bool lockable;
- public Coroutine coroutine;
- public T Value
- {
- get
- {
- if (e != null)
- {
- throw e;
- }
- return returnVal;
- }
- }
- public Coroutine() { lockable = false; }
- public Coroutine(string lockID, float waitTime, LockQueue lockedCoroutines)
- {
- this.lockable = true;
- this.lockID = lockID;
- this.lockedCoroutines = lockedCoroutines;
- this.waitTime = waitTime;
- }
- public IEnumerator InternalRoutine(IEnumerator coroutine)
- {
- if (lockable && lockedCoroutines != null)
- {
- if (lockedCoroutines.Contains(lockID))
- {
- if (waitTime == 0f)
- {
- //Debug.Log(this.GetType().Name + ": coroutine already running and wait not requested so exiting: " + lockID);
- yield break;
- }
- else
- {
- //Debug.Log(this.GetType().Name + ": previous coroutine already running waiting max " + waitTime + " for my turn: " + lockID);
- float starttime = Time.time;
- float counter = 0f;
- lockedCoroutines.Add(lockID, coroutine);
- while (!lockedCoroutines.First(lockID, coroutine) && (Time.time - starttime) < waitTime)
- {
- yield return null;
- counter += Time.deltaTime;
- }
- if (counter >= waitTime)
- {
- string error = this.GetType().Name + ": coroutine " + lockID + " bailing! due to timeout: " + counter;
- Debug.LogError(error);
- this.e = new Exception(error);
- lockedCoroutines.Remove(lockID, coroutine);
- yield break;
- }
- }
- }
- else
- {
- lockedCoroutines.Add(lockID, coroutine);
- }
- }
- while (true)
- {
- try
- {
- if (!coroutine.MoveNext())
- {
- if (lockable) lockedCoroutines.Remove(lockID, coroutine);
- yield break;
- }
- }
- catch (Exception e)
- {
- this.e = e;
- Debug.LogError(this.GetType().Name + ": caught Coroutine exception! " + e.Message + "\n" + e.StackTrace);
- if (lockable) lockedCoroutines.Remove(lockID, coroutine);
- yield break;
- }
- object yielded = coroutine.Current;
- if (yielded != null && yielded.GetType() == typeof(T))
- {
- returnVal = (T)yielded;
- if (lockable) lockedCoroutines.Remove(lockID, coroutine);
- yield break;
- }
- else
- {
- yield return coroutine.Current;
- }
- }
- }
- }
- /// <summary>
- /// coroutine lock and queue
- /// </summary>
- public class LockQueue
- {
- private Dictionary<string, List<IEnumerator>> LockedCoroutines { get; set; }
- public LockQueue()
- {
- LockedCoroutines = new Dictionary<string, List<IEnumerator>>();
- }
- /// <summary>
- /// check if LockID is locked
- /// </summary>
- public bool Contains(string lockID)
- {
- return LockedCoroutines.ContainsKey(lockID);
- }
- /// <summary>
- /// check if given coroutine is first in the queue
- /// </summary>
- public bool First(string lockID, IEnumerator coroutine)
- {
- bool ret = false;
- if (Contains(lockID))
- {
- if (LockedCoroutines[lockID].Count > 0)
- {
- ret = LockedCoroutines[lockID][0] == coroutine;
- }
- }
- return ret;
- }
- /// <summary>
- /// Add the specified lockID and coroutine to the coroutine lockqueue
- /// </summary>
- public void Add(string lockID, IEnumerator coroutine)
- {
- if (!LockedCoroutines.ContainsKey(lockID))
- {
- LockedCoroutines.Add(lockID, new List<IEnumerator>());
- }
- if (!LockedCoroutines[lockID].Contains(coroutine))
- {
- LockedCoroutines[lockID].Add(coroutine);
- }
- }
- /// <summary>
- /// Remove the specified coroutine and queue if empty
- /// </summary>
- public bool Remove(string lockID, IEnumerator coroutine)
- {
- bool ret = false;
- if (LockedCoroutines.ContainsKey(lockID))
- {
- if (LockedCoroutines[lockID].Contains(coroutine))
- {
- ret = LockedCoroutines[lockID].Remove(coroutine);
- }
- if (LockedCoroutines[lockID].Count == 0)
- {
- ret = LockedCoroutines.Remove(lockID);
- }
- }
- return ret;
- }
- }
- }
小結:
本文主要是對 Unity StartCoroutine 進行了理解,從C# 的yileld 和 IEnumerator 到 Unity 的 StartCoroutine,最后並對Cortoutine 進行了擴展,雖然感覺不是很實用(用到的情況非常至少),但還是有利於對Coroutine 的理解和思考。
對於第三部分的代碼感覺有不妥,沒有進行測試,附件里有代碼,有需求的話請自取
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在文首注明出處:http://dsqiu.iteye.com/blog/2049743
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
參考:
①琪琪爸的程序學習筆記 :-P:http://www.cnblogs.com/easyfrog/archive/2011/12/29/IEnumerable_IEnumerator_yield.html
②傑仔:http://www.cnblogs.com/illele/archive/2008/04/21/1164696.html
③Twisted Oak Studios: http://twistedoakstudios.com/blog/Post83_coroutines-more-than-you-want-to-know
④tim tregubov:http://zingweb.com/blog/2013/02/05/unity-coroutine-wrapper/
⑤C# in Depth: http://csharpindepth.com/articles/chapter6/iteratorblockimplementation.aspx
⑥zhw1125: http://blog.sina.com.cn/s/blog_3e29b20b0100g6ix.html