Unity C#筆記 協程詳解(轉)


目錄

  • 什么是協程
  • 多線程
  • 協程
    • 協程的使用場景
    • 協程使用示例
    • 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/

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM