Unity 的多線程、協(xié)程、纖程

  • 在這個(gè)降低入門門檻的大環(huán)境下,Unity 因?yàn)榭紤]到降低門檻,設(shè)計(jì)之初就是一個(gè)單線程,不允許在另外的線程中進(jìn)行渲染等等的工作,不然又要增加很多機(jī)制去處理這個(gè)問題,會(huì)給新來的人徒增很多煩惱。
  • Unity 考慮到 跨平臺(tái)的特性 和引入 異步 的操作,所以提供了另一種異步的手段,就是協(xié)程(Coroutine),通過反編譯,它本質(zhì)上還是在主線程上的優(yōu)化手段,并不屬于真正的 多線程(Thread)。
  • 多線程(Thread)是C#帶來的特性
  • 多線程其實(shí)不難,但同步數(shù)據(jù)是最麻煩的
  • 協(xié)程與纖程都是主線程上的優(yōu)化手段,規(guī)避了異步編程中狀態(tài)機(jī)的復(fù)雜性,使程序邏輯更加簡潔直觀。一個(gè)進(jìn)程可以創(chuàng)建上萬個(gè)協(xié)程。消耗小、切換快。個(gè)人認(rèn)為協(xié)程與纖程其實(shí)本質(zhì)上是一樣的。
  • 如果你的應(yīng)用不需要一些耗時(shí)的操作,比如網(wǎng)絡(luò)請(qǐng)求,IO操作,AI等,那么盡量不要使用多線程(Thread),因?yàn)榭缇€程訪問UI控件是禁止的,并且數(shù)據(jù)同步問題往往也是很棘手的,很容易濫用 lock 導(dǎo)致主線 block 或者 deadlock。反之,如果應(yīng)用程序很復(fù)雜,那么勢必在需要去分擔(dān)主線程的壓力,那么使用異步線程是個(gè)很好的主意。同時(shí),我們也不能濫用線程,過多的使用線程會(huì)造成CPU運(yùn)算的下降,建議使用線程池 ThreadPool 或者利用 GC 來回收線程。

Thread 多線程

線程啟動(dòng)

在Unity中創(chuàng)建一個(gè)異步線程是非常簡單的,直接使用類 System.Threading.Thread 就可以創(chuàng)建一個(gè)線程,線程啟動(dòng)之后畢竟要幫我們?nèi)ネ瓿赡臣虑椤T诰幊填I(lǐng)域,這件事就可以描述了一個(gè)方法,所以需要在構(gòu)造函數(shù)中傳入一個(gè)方法的名稱。

Worker workerObject = new Worker();
Thread workerThread = new Thread(workerObject.DoWork)
workerThread.Start();

線程終止

線程啟動(dòng)很簡單,那么線程終止呢,是不是調(diào)用 Abort 方法。不是,雖然 Thread 對(duì)象提供了
Abort 方法,但并不推薦使用它,因?yàn)樗⒉粫?huì)馬上停止,如果涉及非托管代碼的調(diào)用,還需要等待非托管代碼的處理結(jié)果。

一般停止線程的方法是為線程設(shè)定一個(gè)條件變量,在線程的執(zhí)行方法里設(shè)定一個(gè)循環(huán),并以這個(gè)變量為判斷條件,如果為false則跳出循環(huán),線程結(jié)束。

public class Worker
{
    public void DoWork()
    {
        while (!_shouldStop)
        {
            Console.WriteLine("worker thread: working...");
        }
        Console.WriteLine("worker thread: terminating gracefully.");
    }
    public void RequestStop()
    {
        _shouldStop = true;
    }
    private volatile bool _shouldStop;
}

所以,你可以在應(yīng)用程序退出(OnApplicationQuit)時(shí),將_shouldStop設(shè)置為true來到達(dá)線程的安全退出。

共享數(shù)據(jù)處理

多線程最麻煩的一點(diǎn)就是共享數(shù)據(jù)的處理了,想象一下A,B兩個(gè)線程同一時(shí)刻處理一個(gè)變量,它最終的值到底是什么。所以一般需要使用lock,但C#提供了另一個(gè)關(guān)鍵字 volatile,告訴CPU不讀緩存直接把最新的值返回。所以_shouldStop被volatile修飾。

Dispatcher 調(diào)度員

是不是覺得多線程好簡單,好像也沒想象的那么復(fù)雜,當(dāng)你愉快的在多線程中訪問UI控件時(shí),Duang~~~,一個(gè)錯(cuò)誤告訴你,不能在異步線程訪問UI控件。這是肯定的,跨線程訪問UI控件是不安全的,理應(yīng)被禁止。那怎么辦呢?

注意

  • UnityEngine 的 API 不能在分線程運(yùn)行
  • UnityEngine 定義的基本結(jié)構(gòu)(int, float, struct 定義的數(shù)據(jù)類型)可以在分線程計(jì)算,如 Vector3(struct)可以, 但 Texture2d(class,根父類為 Object) 不可以。
  • UnityEngine 定義的基本類型的函數(shù)可以在分線程運(yùn)行

所以,我們使用 消息通知者生產(chǎn)者-消費(fèi)者模式 的方式告訴一個(gè)在主線程上的 Dispatcher ,來控制 Unity 的組件。
需要把握住幾個(gè)關(guān)鍵點(diǎn):

  • 自己的Dispatcher一定是一個(gè)MonoBehaviour,因?yàn)樵L問UI控件需要在主線程上
  • 什么時(shí)候去更新呢,考慮 生產(chǎn)者-消費(fèi)者模式,有任務(wù)來了,我就是更新到UI上
  • 在Unity中有這么個(gè)方法可以輪詢是不是有任務(wù)要更新,那就是 Update 或者 FixedUpdate 方法,可以根據(jù)需要控制執(zhí)行的周期

生產(chǎn)者-消費(fèi)者模式:
自定義的 UnityDispatcher 提供一個(gè) BeginInvoke 方法,并接送一個(gè) Action

public void BeginInvoke(Action action){
    while (true) {
        //以原子操作的形式,將 32 位有符號(hào)整數(shù)設(shè)置為指定的值并返回原始值。
        if (0 == Interlocked.Exchange (ref _lock, 1)) {
            //acquire lock
            _wait.Enqueue(action);
            _run = true;
            //exist
            Interlocked.Exchange (ref _lock,0);
            break;
        }
    }
}

這是一個(gè)生產(chǎn)者,向隊(duì)列里添加需要處理的Action。有了生產(chǎn)者之后,還需要消費(fèi)者,Unity中的
Update 就是一個(gè)消費(fèi)者,每一幀都會(huì)執(zhí)行,所以如果隊(duì)列里有任務(wù),它就執(zhí)行

 void Update(){

    if (_run) {
        Queue<Action> execute = null;
        //主線程不推薦使用lock關(guān)鍵字,防止block 線程,以至于deadlock
        if (0 == Interlocked.Exchange (ref _lock, 1)) {
        
            execute = new Queue<Action>(_wait.Count);

            while(_wait.Count!=0){

                Action action = _wait.Dequeue ();
                execute.Enqueue (action);

            }
            //finished
            _run=false;
            //release
            Interlocked.Exchange (ref _lock,0);
        }
        //not block
        if (execute != null) {
        
            while (execute.Count != 0) {
            
                Action action = execute.Dequeue ();
                action ();
            }
        }
    
    }
}

值得注意的是,Queue不是線程安全的,所以需要鎖,我使用了Interlocked.Exchange,好處是它以原子的操作來執(zhí)行并且還不會(huì)阻塞線程,因?yàn)橹骶€程本身任務(wù)繁重,所以我不推薦使用lock。

協(xié)程和纖程

Unity 協(xié)程的內(nèi)部原理

對(duì)于Unity應(yīng)用程序而言,還提供了另外一種『異步方式』:Coroutine 。Coroutine 也就是協(xié)程的意思,只是看起來像多線程,它實(shí)際上并不是,還是在主線程上操作。

Coroutine實(shí)際上由 IEnumerator 接口以及一個(gè)或者多個(gè)的 yield 語句構(gòu)成的迭代器(iterator)塊構(gòu)成。

枚舉器接口 IEnumerator 包含3個(gè)方法:

  • Current:返回集合當(dāng)前位置的對(duì)象
  • MoveNext: 把枚舉器位置移到集合的下一個(gè)元素,它返回一個(gè) bool 值,表示新的位置是否超過索引
  • Reset:把位置重置為初始狀態(tài)

yield 是個(gè)比較晦澀的技術(shù),原因是編譯器幫我們做了太多的工作(CompilerGenerate),導(dǎo)致我們無法理解到內(nèi)部的實(shí)現(xiàn)。如果你去翻閱漢英詞典,你會(huì)對(duì) yield 一頭霧水。我個(gè)人傾向?qū)⑵浞g成中斷和產(chǎn)出比較好,這也是 yield 單詞包含的意思,我下面也會(huì)闡述為什么要翻譯成這兩個(gè)意思。

深究 yield 之前,我覺得應(yīng)該略微了解一下為什么我們能 foreach 遍歷一個(gè)數(shù)組?

原因很簡單,數(shù)組 Array 它是一個(gè)可枚舉的類 (enumerable),一個(gè)可枚舉類提供了一個(gè)枚舉器
(enumerator),枚舉器可以依次訪問數(shù)組里的元素,也就是之前提過的 Current 屬性返回集合當(dāng)前位置的對(duì)象。所以,我可以模擬 foreach 的實(shí)現(xiàn),實(shí)際上 foreach 內(nèi)部實(shí)現(xiàn)也大致相似。

static void Main(string[] args)
{
    string[] animals = {"dog", "cat", "pig"};
    //獲取枚舉器
    var ie = animals.GetEnumerator();
    //移到下一項(xiàng),默認(rèn)的index=-1
    while (ie.MoveNext())
    {
        //獲得當(dāng)前項(xiàng)
        Console.WriteLine(ie.Current);
    }
    Console.ReadLine();
}

假設(shè)你是個(gè)C#新手,你得好好消化一下上述的邏輯,因?yàn)檫@是撥開迷霧的第一層:了解為什么能夠枚舉一個(gè)集合。當(dāng)然我們也可以創(chuàng)建自己的可被枚舉的類,需要為它提供自定義的枚舉器,只需實(shí)現(xiàn) IEnumerator 接口即可。值得注意的事,自建的可枚舉類同時(shí)也要實(shí)現(xiàn) IEnumerable 接口,該接口只提供一個(gè)方法:GetEnumerator(),用來返回枚舉器。

創(chuàng)建自定義的枚舉類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]; }
    }
}

你可能會(huì)覺得奇怪,這和 yield 又有什么關(guān)系呢?要解惑 yield 這是第二個(gè)階段:能知道枚舉器是怎樣工作的。

如果你很清楚上訴兩個(gè)階段的內(nèi)部原理之后,要理解 Unity 中的 Coroutine 是非常簡單的,你會(huì)了解為什么它是 偽的 “多線程”。
這是一段非常普通的代碼,司空見慣。

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 方法的放回類型了嗎?沒錯(cuò),返回的就是枚舉器,你會(huì)疑問,你沒有定義一個(gè)枚舉器并且實(shí)現(xiàn)了 IEnumerator 接口?。?/strong>別急,問題就出在 yield 上,C#為了簡化我們創(chuàng)建枚舉器的步驟,你想想看你需要先實(shí)現(xiàn) IEnumerator 接口,并且實(shí)現(xiàn) Current, MoveNext,
Reset 步驟。C#從2.0開始提供了有yield組成的迭代器塊。編譯器會(huì)自動(dòng)更具迭代器塊創(chuàng)建了枚舉器。不信,反編譯看看:

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;
            }
        }

        //...省略...
    }
}

有幾點(diǎn)可以確定:

  • yield是個(gè)語法糖,編譯過后的代碼看不到y(tǒng)ield
  • 編譯器在內(nèi)部創(chuàng)建了一個(gè)枚舉類 <MyEnumerator>d__1
  • yield return 被聲明為枚舉時(shí)的下一項(xiàng),即Current屬性,通過MoveNext方法來訪問結(jié)果

OK,通過層層推進(jìn),想必你對(duì) Untiy中的協(xié)程 有一定的了解了。再回過頭來,我將 yield翻譯成了 中斷產(chǎn)出,談?wù)勎业睦斫狻?/p>

中斷:傳統(tǒng)的方法代碼塊執(zhí)行流程是從上到下依次執(zhí)行,而yield構(gòu)成的迭代塊是告訴編譯器如何創(chuàng)建枚舉器的行為,反編譯得到的結(jié)果可以看到,它們的執(zhí)行并不是連續(xù)的,而是通過switch來從一個(gè)狀態(tài)(state)跳轉(zhuǎn)到另一個(gè)狀態(tài)
產(chǎn)出:yield 是和return連用, yield return之后的語句被編譯器賦值給current變量,最終通過Current屬性產(chǎn)出枚舉項(xiàng)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 寫在開始,講在結(jié)尾。 媽媽,你年輕的時(shí)候,那么年輕。 有人愛慕你年輕時(shí),有人傾心那一段。 你都這么開心的占有著,或...
    隨風(fēng)潛夜閱讀 419評(píng)論 0 0
  • 先來一張大合照~ 不知不覺,21天的課程就要結(jié)束了! 跟著心藍(lán)老師學(xué)到非常詳細(xì)的彩鉛插畫基礎(chǔ)知識(shí),從排線,疊色等等...
    陳少瓊閱讀 800評(píng)論 2 2
  • 時(shí)間太瘦,指縫太寬 我們抓不住金沙,攤開手掌 流年已不見。 我將光陰寫在閏年的腳上 踏足 經(jīng)年碎響 響聲叫不醒我 ...
    考拉家的老王子閱讀 423評(píng)論 0 0
  • 人無百日好,花無百日紅。 路邊一晃而過的紫薇顛覆了這個(gè)說法。 這種花我們這兒原來沒有這種花,后來引種過來。一到夏天...
    海深深閱讀 734評(píng)論 0 1

友情鏈接更多精彩內(nèi)容