在進入本章主題之前,我們必須要了解客戶端應用程式都是單執行緒模型,即只有一個主執行緒(Main Thread),或者叫做UI執行緒,即所有的UI控制元件的建立和操作都是在主執行緒上完成的。而伺服器端應用程式,也就是我們常見的Web應用程式往往是多執行緒的,故使用者A訪問勢必不會影響使用者B的訪問過程。所以對於Web應用而言,多執行緒的資料同步和併發的管理往往是個頭疼的問題。那麼對於客戶端應用程式而言,就一個人使用,還要需要考慮多執行緒嗎?
是否需要多執行緒?
這是個好問題,從裝置的硬體上,這已不是瓶頸:
學過作業系統的同學肯定知道CPU是真正的處理大腦,在單核的CPU年代,在某一時刻CPU只能處理一個執行緒,通過CPU的排程來實現在不同執行緒間切換工作。由於CPU排程的時間很快,所以給人造成併發的假象。
隨著硬體的提升,多核CPU已經是常態化了。比如雙核CPU而言,某一時刻可以有2個執行緒平行計算。
所以,是否需要在客戶端使用多執行緒技術,還是取決於你的應用的複雜度:
- 如果你的應用不需要一些耗時的操作,比如網路請求,IO操作,AI等,那麼儘量不要使用多執行緒,因為跨執行緒訪問UI控制元件是禁止的,並且資料同步問題往往也是很棘手的,很容易濫用
lock
導致主線block或者deadlock。 - 反之,如果應用程式很複雜,那麼勢必在需要去分擔主執行緒的壓力,那麼使用非同步執行緒是個很好的主意。
- 同時,我們也不能濫用執行緒,過多的使用執行緒會造成CPU運算的下降,建議使用執行緒池
ThreadPool
或者利用GC來回收執行緒。
協程的內部原理
回到本文的主題,對於Unity應用程式而言,還提供了另外一種『非同步方式』:Coroutine
。Coroutine
也就是協程的意思,只是看起來像多執行緒,它實際上並不是,還是在主執行緒上操作。
Coroutine實際上由IEnumerator
介面以及一個或者多個的yield
語句構成的迭代器(iterator
)塊構成。
列舉器介面 IEnumerator
包含3個方法:
- Current:返回集合當前位置的物件
- MoveNext:把列舉器位置移到集合的下一個元素,它返回一個bool值,表示新的位置是否超過索引
- Reset:把位置重置為初始狀態
yield
是個比較晦澀的技術,原因是編譯器幫我們做了太多的工作(CompilerGenerate),導致我們無法理解到內部的實現。如果你去翻閱漢英詞典,你會對yield一頭霧水。我個人傾向將其翻譯成中斷和產出比較好,這也是yield單詞包含的意思,我下面也會闡述為什麼要翻譯成這兩個意思。
深究yield
之前,我覺得應該略微瞭解一下為什麼我們能foreach
遍歷一個陣列?
原因很簡單,陣列Array它是一個可列舉的類(enumerable),一個可列舉類提供了一個列舉器(enumerator),列舉器可以依次訪問陣列裡的元素,也就是之前提過的
Current
屬性返回集合當前位置的物件。所以,我可以模擬foreach
的實現,實際上foreach
內部實現也大致相似。
static void Main(string[] args) { string[] animals = {"dog", "cat", "pig"}; //獲取列舉器 var ie = animals.GetEnumerator(); //移到下一項,預設的index=-1 while (ie.MoveNext()) { //獲得當前項 Console.WriteLine(ie.Current); } Console.ReadLine(); }複製程式碼
假設你是個C#新手,你得好好消化一下上述的邏輯,因為這是撥開迷霧的第一層:瞭解為什麼能夠列舉一個集合。當然我們也可以建立自己的可被列舉的類,需要為它提供自定義的列舉器,只需實現
IEnumerator
介面即可。值得注意的事,自建的可列舉類同時也要實現IEnumerable
介面,該介面只提供一個方法:GetEnumerator()
,用來返回列舉器。
建立自定義的列舉類AnimalSet
:
class AnimalSet : IEnumerable
{
private readonly string[] _animals = {"the dog", "the pig", "the cat"};
public IEnumerator GetEnumerator()
{
return new AnimalEnumerator(_animals);
}
}複製程式碼
需要為AnimalSet
提供自定義的列舉器AnimalEnumerator
class AnimalEnumerator : IEnumerator
{
private string[] _animals;
private int _index = -1;
public AnimalEnumerator(string[] animals)
{
_animals=new string[animals.Length];
for (var i = 0; i < animals.Length; i++)
{
_animals[i] = animals[i];
}
}
public bool MoveNext()
{
_index++;
return _index<_animals.Length;
}
public void Reset()
{
_index = -1;
}
public object Current
{
get { return _animals[_index]; }
}
}複製程式碼
你可能會覺得奇怪,這和yield
又有什麼關係呢?要解惑yield
這是第二個階段:能知道列舉器是怎樣工作的。
如果你很清楚上訴兩個階段的內部原理之後,要理解Unity中的Coroutine
是非常簡單的,你會了解為什麼它是偽的“多執行緒”。
這是一段非常普通的程式碼,司空見慣。
void Start()
{
StartCoroutine(MyEnumerator());
Debug.Log("finish");
}
private IEnumerator MyEnumerator()
{
Debug.Log("wait for 1s");
yield return new WaitForSeconds(1);
Debug.Log("wait for 2s");
yield return new WaitForSeconds(2);
Debug.Log("wait for 3s");
yield return new WaitForSeconds(3);
}複製程式碼
注意到MyEnumerator
方法的放回型別了嗎?沒錯,返回的就是列舉器,你會疑問,你沒有定義一個列舉器並且實現了IEnumerator
介面啊!別急,問題就出在yield
上,C#為了簡化我們建立列舉器的步驟,你想想看你需要先實現IEnumerator
介面,並且實現Current
,MoveNext
,Reset
步驟。C#從2.0開始提供了有yield
組成的迭代器塊。編譯器會自動更具迭代器塊建立了列舉器。不信,反編譯看看:
public class Test : MonoBehaviour
{
private IEnumerator MyEnumerator()
{
UnityEngine.Debug.Log("wait for 1s");
yield return new WaitForSeconds(1f);
UnityEngine.Debug.Log("wait for 2s");
yield return new WaitForSeconds(2f);
UnityEngine.Debug.Log("wait for 3s");
yield return new WaitForSeconds(3f);
}
private void Start()
{
base.StartCoroutine(this.MyEnumerator());
UnityEngine.Debug.Log("finish");
}
[CompilerGenerated]
private sealed class <MyEnumerator>d__1 : IEnumerator<object>, IEnumerator, IDisposable
{
private int <>1__state;
private object <>2__current;
public Test <>4__this;
[DebuggerHidden]
public <MyEnumerator>d__1(int <>1__state)
{
this.<>1__state = <>1__state;
}
private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
UnityEngine.Debug.Log("wait for 1s");
this.<>2__current = new WaitForSeconds(1f);
this.<>1__state = 1;
return true;
case 1:
this.<>1__state = -1;
UnityEngine.Debug.Log("wait for 2s");
this.<>2__current = new WaitForSeconds(2f);
this.<>1__state = 2;
return true;
case 2:
this.<>1__state = -1;
UnityEngine.Debug.Log("wait for 3s");
this.<>2__current = new WaitForSeconds(3f);
this.<>1__state = 3;
return true;
case 3:
this.<>1__state = -1;
return false;
}
return false;
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return this.<>2__current;
}
}
//...省略...
}
}複製程式碼
有幾點可以確定:
yield
是個語法糖,編譯過後的程式碼看不到yield
- 編譯器在內部建立了一個列舉類
<MyEnumerator>d__1
yield return
被宣告為列舉時的下一項,即Current屬性,通過MoveNext方法來訪問結果
OK,通過層層推進,想必你對Untiy中的協程有一定的瞭解了。再回過頭來,我將yield
翻譯成了中斷和產出,談談我的理解。
- 中斷:傳統的方法程式碼塊執行流程是從上到下依次執行,而
yield
構成的迭代塊是告訴編譯器如何建立列舉器的行為,反編譯得到的結果可以看到,它們的執行並不是連續的,而是通過switch
來從一個狀態(state)跳轉到另一個狀態 - 產出:
yield
是和return
連用,yield return
之後的語句被編譯器賦值給current變數,最終通過Current
屬性產出列舉項
小結
本文的初衷是想介紹如何在Unity中使用多執行緒,但協程往往是繞不開的話題,於是索性就剖析了下它,故決定單獨成一篇。本章內容對多執行緒開了個頭,我將在下篇文章中說說怎樣在Unity中使用和管理多執行緒。
原始碼託管在Github上,點選此瞭解
歡迎關注我的公眾號: