在Unity中StartCoroutine/yield return這個模式到底是怎麼應用的?其中的原理是什麼?
瀉藥!
我從C/C++轉向Unity的C#過程中,StartCoroutine/yield return 也是我一開始沒弄明白的東西之一。
我在網上收集資料的過程中,有兩篇文章是讓我有醍醐灌頂的感覺的。
在此拾人牙慧,希望 tkokof1的專欄 能夠幫到你。
Coroutine,你究竟幹了什麼?
Coroutine,你究竟幹了什麼?(小續)
說到Coroutine,我們必須提到兩個更遠的東西。在操作系統(os)級別,有進程(process)和線程(thread)兩個(僅從我們常見的講)實際的「東西」(不說概念是因為這兩個傢伙的確不僅僅是概念,而是實際存在的,os的代碼管理的資源)。這兩個東西都是用來模擬「並行」的,寫操作系統的程序員通過用一定的策略給不同的進程和線程分配CPU計算資源,來讓用戶「以為」幾個不同的事情在「同時」進行「。在單CPU上,是os代碼強制把一個進程或者線程掛起,換成另外一個來計算,所以,實際上是串列的,只是「概念上的並行」。在現在的多核的cpu上,線程可能是「真正並行的」。
Coroutine,翻譯成」協程「,初始碰到的人馬上就會跟上面兩個概念聯繫起來。直接先說區別,Coroutine是編譯器級的,Process和Thread是操作系統級的。Coroutine的實現,通常是對某個語言做相應的提議,然後通過後成編譯器標準,然後編譯器廠商來實現該機制。Process和Thread看起來也在語言層次,但是內生原理卻是操作系統先有這個東西,然後通過一定的API暴露給用戶使用,兩者在這裡有不同。Process和Thread是os通過調度演算法,保存當前的上下文,然後從上次暫停的地方再次開始計算,重新開始的地方不可預期,每次CPU計算的指令數量和代碼跑過的CPU時間是相關的,跑到os分配的cpu時間到達後就會被os強制掛起。Coroutine是編譯器的魔術,通過插入相關的代碼使得代碼段能夠實現分段式的執行,重新開始的地方是yield關鍵字指定的,一次一定會跑到一個yield對應的地方。
對於Coroutine,下面是一個實現的function,裡面的片段被yield關鍵字分成2段:
IEnumerator YieldSomeStuff()
{
yield "hello";
Console.WriteLine("foo!");
yield "world";
}
推進的代碼(模擬,非實際):
IEnumerator e = YieldSomeStuff();
while(e.MoveNext())
{
Console.WriteLine(e.Current);
}
以此來推進整個代碼片段的分段執行。更詳細的分析如 @鄧凱的文章里提到。這裡只要說明的是,對於Coroutine,是編譯器幫助做了很多的事情,來讓代碼不是一次性的跑到底,而不是操作系統強制的掛起。代碼每次跑多少,是可預期的。但是,Process和Thread,在這個層面上完全不同,這兩個東西是操作系統管理的。在unity中,StartCoroutine這個方法是個推進器。StartCoroutine會發起類似上面的while循環。因為是while循環,因此,Coroutine本身其實不是「非同步的」。
Coroutine在整個Unity系統的位置,下面一張圖可以說明:
Unity官方文檔里也寫到"Normal Coroutine在Update之後"的字眼,如下內容第一行:
Normal coroutine updates are run after the Update function returns. A coroutine is a function that can suspend its execution (yield) until the given YieldInstruction finishes. Different uses of Coroutines:
yield; The coroutine will continue after all Update functions have been called on the next frame.
yield WaitForSeconds(2); Continue after a specified time delay, after all Update functions have been called for the frame
yield WaitForFixedUpdate(); Continue after all FixedUpdate has been called on all scripts
yield WWW Continue after a WWW download has completed.
yield StartCoroutine(MyFunc); Chains the coroutine, and will wait for the MyFunc coroutine to complete first.
得票最多的是最長的呢,其實鄧凱先生推薦的兩篇文章就很好,penguin ku先生的解釋也是正確的,不過IEnumerator不是和foreach類似,而是foreach就是調用的MoveNext方法。
系統沒有現成的協程的話用枚舉器模擬協程還是很常見的,python的枚舉器甚至為這種用法支持了yield帶參數的語法。C#沒有跟風大概是因為4.0向後有Task類支持類似協程的概念了吧,await/async生成的狀態機也是和枚舉器差不多的東西,雖然和當代Unity沒關係就是了。
Unity的協程只是每幀MoveNext而已,就這樣。
我是寫傳統C++遊戲轉過來的,StartCoroutine這個特性非常爽,解決了兩個問題:1,需要大量計算量的時候,不會集中在一幀內執行完,可以將計算量分攤到後面的幀。比如對大量3D頂點的處理,直接寫一個循環,運行的時候會在這裡卡一下。但是使用StartCoroutine,就會把計算量平坦到後面的幀中。2,在傳統遊戲中,有時候需要在updata中穿插大量邏輯,特別亂。StartCoroutine讓這個過程簡化了。
不要被樓上那麼多話嚇到。不要去理解進程線程以及任務等。Coroutine的參數都是IEnumerator類型的,你看下這個interface,他有Current屬性,MoveNext方法,實際上操作類似foreach,即每次movenext下,得到current,執行下current,然後等下一幀被調用,執行過程還是如此,直到MoveNext返回false,表示執行完畢了,釋放掉IEnumerator即可。所以,你可以弄個GameObejct試驗下,給他弄個Coroutine,然後半路把這個GameObejct殺了,你會發現Coroutine雖然沒執行完,但也沒了,因為沒人再去調用了。很好理解的東西
yield return兩個字:等待。等待一幀,等待一秒,等待其他協程完成……當你明白了什麼叫等待,你就領悟了協程的真諦。
推薦閱讀:
※Vert.x性能如何,如何評價Vert.x萬事皆非同步的特性?
※怎麼看:Python 3.5 支持 async/await ?