目錄
- 什么是協程
- 多線程
- 協程
- 協程的使用場景
- 協程使用示例
- Invoke的缺陷
- 協程語法
- 開啟協程
- 終止協程
- 掛起
- 協程的執行原理
什么是協程
在Unity中,協程(Coroutines)的形式是我最喜歡的功能之一,我都會使用它來控制需要定時的。
協同程序,在主程序運行的同時,開啟另外一段邏輯處理,來協同當前程序的執行。
可能看了這段文字介紹還是有點模糊,其實可以用多線程來比較。
多線程
多線程,顧名思義,多條同時執行的線程。
最初,多線程的誕生是為了解決IO阻塞問題,如今多線程可以解決許多同樣需要異步方法的問題(例如網絡等)。
所謂異步,通俗點講,就是我走我的線程,你走你的線程。當某個線程阻塞時,另一個線程不會受影響繼續執行。
需要認識到的是,多線程並不是真正意義上的多條線程同時執行。
它的實際是將一個時間段分成若干個時間片,每個線程輪流運行一個時間片。
(如圖,將執行步驟切分成極小的粒度,然后依次運行)
但是由於時間片粒度非常非常小,幾乎看不出區別,所以程序執行效果跟真正意義上的並行執行效果基本一致。
多線程的缺陷
然而多線程有一個壞處,就是可能造成共享數據的沖突。
假如有一個變量i = 0, Step1_1的操作是進行++i操作,Step2_1的操作是進行--i操作。
我們預期最終結果i為0。
但由於操作切分得過小,可能會發生這樣順序的事:
- 線程1:訪問i, 將0存到寄存器
- 線程2:訪問i, 將0存到寄存器
- 線程1:++i, 得到1
- 線程2:--i, 得到-1
- 線程1:將1寫入到i的內存
- 線程2:將-1寫入到i的內存
- 最終i的值為-1
當然多線程的沖突也有解決方案: 互斥鎖....
但是這些多多少少會付出額外的代價,讓程序變得臃腫。
協程
CPU有多條線程,一條線程可以有多個協程。
協程跟多線程類似,也有類似異步的效果(注意不是真正的異步)。
只不過它的切分粒度不是基於系統划分的時間片,而是基於我們編寫的yield,而且往往粒度更大。
粒度是取決於自己定義什么時候讓協程掛起:
//下面定義了一個協程函數,注意必須使用IEnumerator作為返還值才能成為協程函數。 IEnumerator Test() { for(int i = 0; i<1000 ; ++i){ ans += i; yield return 0;//掛起,下一幀再來從這個位置繼續執行。 } j+=2; yield return 0;//掛起,下一幀再來從這個位置繼續執行。 ++j; yield return 0;//掛起,下一幀再來從這個位置繼續執行。 }
如果划分的粒度過大,協程所在的線程可能在相應的幀卡頓。
甚至如果讓協程阻塞(死循環),那么協程所在的整個線程也會阻塞。
因此說協程可以有類似異步的效果,但是不是真正的異步。
協程的一大好處就是可以避免數據訪問沖突的問題:
因為它的粒度相對多線程的大很多,所以往往很少出現沖突現象
在上面多線程的例子里,使用協程則可以這樣:
- Step1_1: 執行完++i, 此時i=1
- Step2_1: 執行完--i, 此時i=0
- 最終i的值為0
協程的使用場景
對於保證不會阻塞的並行操作且並行性要求不高的並行操作,可以使用協程。
更實際來說,協程最常用於延時執行等控制時間軸的操作,例如N秒后調用指定函數。
利用每幀執行一段協程的特性,我們可以引入個帶累加計時判斷循環,然后再超過3秒后跳出循環,執行Debug.Log()
//3s后執行Debug.Log IEnumerator Test() { for(float timer = 0.0f; timer < 3.0f ; timer += Time.DeltaTime){ yield return 0;//掛起,下一幀再來從這個位置繼續執行。 } Debug.Log("啟動協程3s后"); }
但是Unity封裝了個更好用的類:WaitForSeconds
使這種延時的協程代碼更加簡潔。
//原本寫法 for(float timer = 0.0f; timer < 3.0f ; timer += Time.DeltaTime){ yield return 0;//掛起,下一幀再來從這個位置繼續執行。 } //使用WaitForSeconds的寫法 yield return new WaitForSeconds(3.0f);
協程使用示例
接下來就展示下,協程使用的示例:
首先編寫好協程函數
IEnumerator TestWaitForSeconds() { //3s后執行Debug.Log; yield return new WaitForSeconds(3.0f); Debug.Log("啟動協程3s后"); }
然后在某個地方使用StartCoroutine(TestWaitForSeconds())或者StartCoroutine("TestWaitForSeconds")
//啟動協程:3s后執行Debug.log StartCoroutine(TestWaitForSeconds()); //啟動后,繼續往下執行 ...
Invoke的缺陷
另外一提,Unity還有個一樣也是用於延時調用的函數,叫Invoke
Invoke("test",2.0f); \\延時2秒后執行函數test
但是Invock所要調用的函數必須是空類型返還值,還必須得是在當前類里面的方法。
一般來說,用協程來解決這樣的問題已經綽綽有余,而且還有更安全的調用方法而不是只用string類型作為參數的方法,因此沒必要使用Invoke。
協程語法
開啟協程
StartCoroutine(string methodName);
- 參數是方法名(字符串類型),此方法可以包含一個參數。
- 形參方法可以有返回值
StartCoroutine(IEnumerator method);
- 參數是方法(TestMethod()),此方法中可以包含多個參數。
- IEnumrator類型的方法不能含有ref或者out類型的參數,但可以含有被傳遞的引用
- 形參方法必須有返回值,且返回值類型為IEnumrator,返回值使用(yield retuen +表達式或者值,或者 yield break)語句
終止協程
StopCoroutine(string methodName);//終止指定的協程
- 在程序中調用StopCoroutine()方法只能終止以字符串形式啟動的協程
StopAllCoroutine();//終止所有協程
掛起
//程序在下一幀中從當前位置繼續執行 yield return 0; //程序在下一幀中從當前位置繼續執行 yield return null; //程序等待N秒后從當前位置繼續執行 yield return new WaitForSeconds(N); //在所有的渲染以及GUI程序執行完成后從當前位置繼續執行 yield new WaitForEndOfFrame(); //所有腳本中的FixedUpdate()函數都被執行后從當前位置繼續執行 yield new WaitForFixedUpdate(); //等待一個網絡請求完成后從當前位置繼續執行 yield return WWW; //等待一個xxx的協程執行完成后從當前位置繼續執行 yield return StartCoroutine(xxx); //如果使用yield break語句,將會導致協程的執行條件不被滿足,不會從當前的位置繼續執行程序,而是直接從當前位置跳出函數體,回到函數的根部 yield break;
協程的執行原理
協程函數的返回值時IEnumerator,它是一個迭代器,可以把它當成執行一個序列的某個節點的指針。
它提供了兩個重要的接口,分別是Current(返回當前指向的元素)和MoveNext()(將指針向后移動一個單位,如果移動成功,則返回true)。
yield關鍵詞用來聲明序列中的下一個值或者是一個無意義的值。
如果使用yield return x(x是指一個具體的對象或者數值)的話,
那么MoveNext返回為true並且Current被賦值為x,如果使用yield break使得MoveNext()返回為false。
如果MoveNext函數返回為true意味着協程的執行條件被滿足,則能夠從當前的位置繼續往下執行。否則不能從當前位置繼續往下執行。
作者:KillerAery 出處:http://www.cnblogs.com/KillerAery/
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。