unity 多線程


在進入本章主題之前,我們必須要了解客戶端應用程序都是單線程模型,即只有一個主線程(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應用程序而言,還提供了另外一種『異步方式』:CoroutineCoroutine也就是協程的意思,只是看起來像多線程,它實際上並不是,還是在主線程上操作。

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,MoveNextReset步驟。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上,點擊此了解

本博客為 木宛城主原創,基於 Creative Commons Attribution 2.5 China Mainland License發布,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名 木宛城主(包含鏈接)。如您有任何疑問或者授權方面的協商,請給我留言。


免責聲明!

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



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