Unity 中協程是個非常強大的功能,其作用主要是用於游戲中的延時調用或者執行一連串的有時間間隔的事件流程,例如劇情對話等。簡單總結了幾點協程相關的知識點,旨在加深記憶,同時為初學者解惑。
1、協程、進程與線程
這是個面試中經常會問到的問題:協程、進程與線程的區別在哪?
說到協程,我們首先回顧以下線程與進程這兩個概念。在操作系統(os)級別,有進程(process)和線程(thread)兩個我們看不到但又實際存在的“東西”,這兩個東西都是用來模擬“並行”的,寫操作系統的程序員通過用一定的策略給不同的進程和線程分配CPU計算資源,來讓用戶“以為”幾個不同的事情在“同時”進行“。在單CPU上,是os代碼強制把一個進程或者線程掛起,換成另外一個來計算,所以,實際上是串行的,只是“概念上的並行”。在現在的多核的cpu上,線程可能是“真正並行的”。
進程擁有自己獨立的堆和棧,既不共享堆,亦不共享棧,進程由操作系統調度。
線程擁有自己獨立的棧和共享的堆,共享堆,不共享棧,線程亦由操作系統調度(標准線程是的)。
協程和線程一樣共享堆,不共享棧,協程由程序員在協程的代碼里顯示調度。
一個應用程序一般對應一個進程,一個進程一般有一個主線程,還有若干個輔助線程,線程之間是平行運行的,在線程里面可以開啟協程,讓程序在特定的時間內運行。
協程和線程的區別是:協程避免了無意義的調度,由此可以提高性能,但也因此,程序員必須自己承擔調度的責任,同時,協程也失去了標准線程使用多CPU的能力。
打個比方吧,假設有一個操作系統,是單核的,系統上沒有其他的程序需要運行,有兩個線程 A 和 B ,A 和 B 在單獨運行時都需要 10 秒來完成自己的任務,而且任務都是運算操作,A B 之間也沒有競爭和共享數據的問題。現在 A B 兩個線程並行,操作系統會不停的在 A B 兩個線程之間切換,達到一種偽並行的效果,假設切換的頻率是每秒一次,切換的成本是 0.1 秒(主要是棧切換),總共需要 20 + 19 * 0.1 = 21.9 秒。如果使用協程的方式,可以先運行協程 A ,A 結束的時候讓位給協程 B ,只發生一次切換,總時間是 20 + 1 * 0.1 = 20.1 秒。如果系統是雙核的,而且線程是標准線程,那么 A B 兩個線程就可以真並行,總時間只需要 10 秒,而協程的方案仍然需要 20.1 秒。
其實就根本來說,協程除了名字之外,和線程是沒有太大聯系的。Unity中的特殊在於所有的腳本和代碼都是在一個主線程里運行的,協程也不例外。協程與線程的相似點只在於,協程看起來也可以與其他函數並行執行。 但本質上來說,線程是通過可以開啟多個子線程同時執行程序,而達到並行。而協程則是通過每幀檢測的方式,在自己與其他函數之間切換。這種“來回跑”的方式,與Unity中一慣有明確執行順序的風格(腳本生命周期)看起來不太統一。但這正是它的強大之處,使得我們在使用協程的時候不必考慮lock等諸多線程中的問題。
2、協程執行原理
unity中協程執行過程中,通過yield return XXX,將程序掛起,去執行接下來的內容,注意協程不是線程,在為遇到yield return XXX語句之前,協程額方法和一般的方法是相同的,也就是程序在執行到yield return XXX語句之后,接着才會執行的是 StartCoroutine()方法之后的程序,走的還是單線程模式,僅僅是將yield return XXX語句之后的內容暫時掛起,等到特定的時間才執行。
那么掛起的程序什么時候才執行,這就要看monoBehavior的生命周期了。
也就是協同程序主要是update()方法之后,lateUpdate()方法之前調用的,接下來我們通過一個小例子去理解一下。
using UnityEngine; using System.Collections; using System.Threading; public class test : MonoBehaviour { void Start() { StartCoroutine(tt());//開啟協程 for (int i = 0; i < 200; i++) //循環A { Debug.Log("*************************" + i); Thread.Sleep(10); } } IEnumerator tt() { for (int i = 0; i < 100; i++) //循環B { Debug.Log("-------------------" + i); } yield return new WaitForSeconds(1); //協程1 for (int i = 0; i < 100; i++) //循環C { Debug.Log(">>>>>>>>>>>>>>>>>>>>" + i); yield return null; //協程1 } } // 更新數據 void Update() { Debug.Log("Update"); } //晚於更新 void LateUpdate() { Debug.Log("------LateUpdate"); } }
程序的運行結果為:
先執行循環B,然后執行循環A,然后執行update()和lateUpdate()的方法,等待1S之后,在updat()和lateupda()之間執行循環C的輸出。
3、yield return 的不同返回類型
使用yield return的時候你會發現它可以返回的類型一長串,對於初學者我覺得就分為帶 new和不帶new的就行了。
先說不帶new的。通常可以yield return的有 null,數字 ,字符串,布爾值甚至表達式,函數,嵌套協程等。
以在Start()中開啟當前協程為例,如果是不帶new的返回類型,執行時間都是一樣的。即在第一時間執行協程中的代碼 到第一個yield return當行為止,然后在下一幀的Update之后,LateUpdate之前執行yield return后面的代碼。
另外需要注意的是,yield return后面可以是一個函數調用,賦值表達式,嵌套的其它協程等。以賦值的表達式num=10為例;它會在當行yield return執行的時候就執行,函數調用和其它協程也是一樣。也就是說,此時yield return的函數調用就相當於直接調用了這個函數,並且是當時就執行的。 而其它return 類型 如null,字符串,數字等一般只用作延遲一幀來用,其它作用,待我后期再研究下。
下面說帶new的,也是通常我們重點使用的協程功能。
這里列舉幾個:
(1)new WaitUntil(Func<bool>) 參數是一個布爾返回類型的委托,作用是,知道這個返回的布爾值為true時,協程才會繼續執行當行yield return 后面的代碼。
(2) new WaitForSeconds(float)參數是float類型的數字,表示秒,也是協程最常用的功能之一。 作用是,在N秒后才會繼續執行當行yield return 后面的代碼。
由於yield return可以在一個協程中任意位置寫多個,配合這個可以實現很多時間細化可視化的功能。
(3)new WaitForEndOfFrame()作用是,在結束當前幀 攝像機和GUI被渲染以及其它函數完成后才會繼續執行當行yield return 后面的代碼。 這個我只驗證了在LateUpdate執行完之后執行,具體在整個腳本周期中哪個函數執行完之后開始執行還未詳細驗證。
(4)new WaitForFixedUpdate() 作用是,直到當行代碼之后第一個FixedUpdate執行之后才會繼續執行當行yield return 后面的代碼。也就是說,如果是在start里面開啟協程的話,第一次執行FixedUpdate之后就會繼續執行return后面的代碼。
后面還有許多類型的 返回,沒有一一驗證,不過作用應該大同小異,即在執行第一個該類型動作之后才會繼續執行當行yield return 后面的代碼。
值得一提的是,協程的延遲調用和非阻塞式掛起是用於網絡請求等高級結構很好的工具,非常值得花一些時間去仔細研究。
4、開始協程
通過MonoBehaviour提供的StartCoroutine方法來實現啟動協同程序。
1、StartCoroutine(IEnumerator routine);
優點:靈活,性能開銷小。
缺點:無法單獨的停止這個協程,如果需要停止這個協程只能等待協同程序運行完畢或則使用StopAllCoroutine();方法。
2、StartCoroutine (methodName:string, value : object = null);
優點:可以直接通過傳入協同程序的方法名來停止這個協程:StopCoroutine(string methodName);
缺點:性能的開銷較大,只能傳遞一個參數。
5、停止協程
協程內停止可以用yield return break;
協程外:
1、StopCoroutine(string methodName);
2、StopAllCoroutine();
3、設置gameobject的active為false時可以終止協同程序,但是再次設置為true后協程不會再啟動。設置當前協程所在腳本enable為false也並不能停止當前協程的執行。