Unity C#筆記 協程


什麼是協程

在Unity中,協程(Coroutines)的形式是我最喜歡的功能之一,我都會使用它來控制須要定時的。網絡

協同程序,在主程序運行的同時,開啓另一段邏輯處理,來協同當前程序的執行。
可能看了這段文字介紹仍是有點模糊,其實能夠用多線程來比較。多線程

多線程

多線程,顧名思義,多條同時執行的線程。
最初,多線程的誕生是爲了解決IO阻塞問題,現在多線程能夠解決許多一樣須要異步方法的問題(例如網絡等)。
所謂異步,通俗點講,就是我走個人線程,你走你的線程。當某個線程阻塞時,另外一個線程不會受影響繼續執行。異步

須要認識到的是,多線程並非真正意義上的多條線程同時執行。
它的實際是將一個時間段分紅若干個時間片,每一個線程輪流運行一個時間片。函數

(如圖,將執行步驟切分紅極小的粒度,而後依次運行)線程

可是因爲時間片粒度很是很是小,幾乎看不出區別,因此程序執行效果跟真正意義上的並行執行效果基本一致。指針

多線程的缺陷

然而多線程有一個壞處,就是可能形成共享數據的衝突。code

假若有一個變量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意味着協程的執行條件被知足,則可以從當前的位置繼續往下執行。不然不能從當前位置繼續往下執行。

相關文章
相關標籤/搜索