Unity協程(Coroutine)原理深刻剖析再續html
By D.S.Qiuios
尊重他人的勞動,支持原創,轉載請註明出處:http.dsqiu.iteye.comweb
前面已經介紹過對協程(Coroutine)的認識和理解,主要講到了Unity引擎在執行協程(Coroutine)的原理(Unity協程(Coroutine)原理深刻剖析)和對協程(Coroutine)狀態的控制(Unity協程(Coroutine)管理類——TaskManager工具分享),到這使用Coroutine的疑問就沒有了,可是D.S.Qiu仍是有點沒嚼爛,因此以爲頗有必要再續。express
本文主要分爲三部分:api
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.app
1)yield return, IEnumerator 和 Unity StartCoroutine 的關係和理解函數
yield 和 IEnumerator都是C#的東西,前者是一個關鍵字,後者是枚舉類的接口。對於IEnumerator 只引用②對 IEnumerable與IEnumerator區別 的論述:
先貼出 IEnumerable 和 IEnumerator的定義:
IEnumerable和IEnumerator有什麼區別?這是一個很讓人困惑的問題(在不少forum裏都看到有人在問這個問題)。研究了半天,獲得如下幾點認識:
一、一個Collection要支持foreach方式的遍歷,必須實現IEnumerable接口(亦即,必須以某種方式返回IEnumerator object)。
二、IEnumerator object具體實現了iterator(經過MoveNext(),Reset(),Current)。
三、從這兩個接口的用詞選擇上,也能夠看出其不一樣:IEnumerable是一個聲明式的接口,聲明實現該接口的class是「可枚舉(enumerable)」的,但並無說明如何實現枚舉器(iterator);IEnumerator是一個實現式的接口,IEnumerator object就是一個iterator。
四、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的函數獲得的:
在函數內使用前面介紹 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 的處理,這裏僅附上一個例子:
C#編譯器對應生成:
從上面的C#實現能夠知道:函數內有多少個 yield return 在對應的 MoveNext() 就會返回多少次 true (不包含嵌套)。另外很是重要的一點的是:同一個函數內的其餘代碼(不是 yield return 語句)會被移到 MoveNext 中去,也就是說,每次 MoveNext 都會順帶執行同一個函數中 yield return 以前,以後 和兩個 yield return 之間的代碼。
對於Unity 引擎的 YieldInstruction 實現,其實就能夠看着一個 函數體,這個函數體每幀會實現去 check MoveNext 是否返回 false 。 例如:
上面這行代碼的僞代碼實現:
增補於: 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 值和進行錯誤捕獲處理。
先定義封裝包裹返回值和錯誤信息的類:
InteralRoutine是對返回 Current 值和拋出的異常信息(若是有的話):
下面爲這個類擴展MonoBehavior:
最後給出一個 Example:
最後輸出是10,由於Cortoutine<T> 遇到知足條件的 T 類型就 執行 yield break;就不執行 yield return 5; 這條語句了。
若是將中 yield break; 語句去掉的話,最後輸出的是 5 而不是10。
其實就是Unity引擎每幀去 check yield return 後面的表達式,若是知足就繼續向下執行。
下面在測試一個例子:連續兩次調用 yield return coroutine;
測試運行會發現只會輸出:
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 方法,下面直接貼出代碼:
小結:
本文主要是對 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