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

  • 在這個降低入門門檻的大環(huán)境下郑趁,Unity 因為考慮到降低門檻,設計之初就是一個單線程姿搜,不允許在另外的線程中進行渲染等等的工作寡润,不然又要增加很多機制去處理這個問題,會給新來的人徒增很多煩惱舅柜。
  • Unity 考慮到 跨平臺的特性 和引入 異步 的操作梭纹,所以提供了另一種異步的手段,就是協(xié)程(Coroutine)致份,通過反編譯变抽,它本質上還是在主線程上的優(yōu)化手段,并不屬于真正的 多線程(Thread)
  • 多線程(Thread)是C#帶來的特性
  • 多線程其實不難绍载,但同步數(shù)據(jù)是最麻煩的
  • 協(xié)程與纖程都是主線程上的優(yōu)化手段诡宗,規(guī)避了異步編程中狀態(tài)機的復雜性,使程序邏輯更加簡潔直觀击儡。一個進程可以創(chuàng)建上萬個協(xié)程塔沃。消耗小、切換快阳谍。個人認為協(xié)程與纖程其實本質上是一樣的蛀柴。
  • 如果你的應用不需要一些耗時的操作,比如網(wǎng)絡請求矫夯,IO操作鸽疾,AI等,那么盡量不要使用多線程(Thread)茧痒,因為跨線程訪問UI控件是禁止的肮韧,并且數(shù)據(jù)同步問題往往也是很棘手的,很容易濫用 lock 導致主線 block 或者 deadlock旺订。反之弄企,如果應用程序很復雜,那么勢必在需要去分擔主線程的壓力区拳,那么使用異步線程是個很好的主意拘领。同時,我們也不能濫用線程樱调,過多的使用線程會造成CPU運算的下降约素,建議使用線程池 ThreadPool 或者利用 GC 來回收線程。

Thread 多線程

線程啟動

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

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

線程終止

線程啟動很簡單爪模,那么線程終止呢欠啤,是不是調用 Abort 方法。不是屋灌,雖然 Thread 對象提供了
Abort 方法洁段,但并不推薦使用它,因為它并不會馬上停止共郭,如果涉及非托管代碼的調用祠丝,還需要等待非托管代碼的處理結果疾呻。

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

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

所以散吵,你可以在應用程序退出(OnApplicationQuit)時,將_shouldStop設置為true來到達線程的安全退出蟆肆。

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

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

Dispatcher 調度員

是不是覺得多線程好簡單淤齐,好像也沒想象的那么復雜股囊,當你愉快的在多線程中訪問UI控件時,Duang~~~更啄,一個錯誤告訴你稚疹,不能在異步線程訪問UI控件。這是肯定的祭务,跨線程訪問UI控件是不安全的内狗,理應被禁止。那怎么辦呢义锥?

注意

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

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

  • 自己的Dispatcher一定是一個MonoBehaviour贰拿,因為訪問UI控件需要在主線程上
  • 什么時候去更新呢,考慮 生產(chǎn)者-消費者模式熄云,有任務來了膨更,我就是更新到UI上
  • 在Unity中有這么個方法可以輪詢是不是有任務要更新,那就是 Update 或者 FixedUpdate 方法缴允,可以根據(jù)需要控制執(zhí)行的周期

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

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

這是一個生產(chǎn)者矗漾,向隊列里添加需要處理的Action锈候。有了生產(chǎn)者之后,還需要消費者敞贡,Unity中的
Update 就是一個消費者泵琳,每一幀都會執(zhí)行,所以如果隊列里有任務誊役,它就執(zhí)行

 void Update(){

    if (_run) {
        Queue<Action> execute = null;
        //主線程不推薦使用lock關鍵字获列,防止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í)行并且還不會阻塞線程巩梢,因為主線程本身任務繁重,所以我不推薦使用lock艺玲。

協(xié)程和纖程

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

對于Unity應用程序而言括蝠,還提供了另外一種『異步方式』:Coroutine 。Coroutine 也就是協(xié)程的意思板驳,只是看起來像多線程又跛,它實際上并不是,還是在主線程上操作若治。

Coroutine實際上由 IEnumerator 接口以及一個或者多個的 yield 語句構成的迭代器(iterator)塊構成慨蓝。

枚舉器接口 IEnumerator 包含3個方法:

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

yield 是個比較晦澀的技術礼烈,原因是編譯器幫我們做了太多的工作(CompilerGenerate),導致我們無法理解到內(nèi)部的實現(xiàn)婆跑。如果你去翻閱漢英詞典此熬,你會對 yield 一頭霧水。我個人傾向將其翻譯成中斷和產(chǎn)出比較好滑进,這也是 yield 單詞包含的意思,我下面也會闡述為什么要翻譯成這兩個意思扶关。

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

原因很簡單节槐,數(shù)組 Array 它是一個可枚舉的類 (enumerable)搀庶,一個可枚舉類提供了一個枚舉器
(enumerator)拐纱,枚舉器可以依次訪問數(shù)組里的元素,也就是之前提過的 Current 屬性返回集合當前位置的對象哥倔。所以秸架,我可以模擬 foreach 的實現(xiàn),實際上 foreach 內(nèi)部實現(xiàn)也大致相似咆蒿。

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#新手,你得好好消化一下上述的邏輯蜡秽,因為這是撥開迷霧的第一層:了解為什么能夠枚舉一個集合府阀。當然我們也可以創(chuàng)建自己的可被枚舉的類,需要為它提供自定義的枚舉器芽突,只需實現(xiàn) IEnumerator 接口即可试浙。值得注意的事,自建的可枚舉類同時也要實現(xiàn) IEnumerable 接口寞蚌,該接口只提供一個方法: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]; }
    }
}

你可能會覺得奇怪挟秤,這和 yield 又有什么關系呢壹哺?要解惑 yield 這是第二個階段:能知道枚舉器是怎樣工作的。

如果你很清楚上訴兩個階段的內(nèi)部原理之后艘刚,要理解 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 方法的放回類型了嗎?沒錯秋度,返回的就是枚舉器炸庞,你會疑問,你沒有定義一個枚舉器并且實現(xiàn)了 IEnumerator 接口凹运埂埠居!別急,問題就出在 yield 上事期,C#為了簡化我們創(chuàng)建枚舉器的步驟滥壕,你想想看你需要先實現(xiàn) IEnumerator 接口,并且實現(xiàn) Current, MoveNext兽泣,
Reset 步驟绎橘。C#從2.0開始提供了有yield組成的迭代器塊。編譯器會自動更具迭代器塊創(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;
            }
        }

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

有幾點可以確定:

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

OK,通過層層推進枷餐,想必你對 Untiy中的協(xié)程 有一定的了解了靶瘸。再回過頭來,我將 yield翻譯成了 中斷產(chǎn)出毛肋,談談我的理解怨咪。

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

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子击罪,更是在濱河造成了極大的恐慌痒给,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件璃饱,死亡現(xiàn)場離奇詭異与斤,居然都是意外死亡,警方通過查閱死者的電腦和手機荚恶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門撩穿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人裆甩,你說我怎么就攤上這事冗锁。” “怎么了嗤栓?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵冻河,是天一觀的道長。 經(jīng)常有香客問我茉帅,道長叨叙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任堪澎,我火速辦了婚禮擂错,結果婚禮上,老公的妹妹穿的比我還像新娘樱蛤。我一直安慰自己钮呀,他們只是感情好剑鞍,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著爽醋,像睡著了一般蚁署。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蚂四,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天光戈,我揣著相機與錄音,去河邊找鬼遂赠。 笑死久妆,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的跷睦。 我是一名探鬼主播筷弦,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼抑诸!你這毒婦竟也來了奸笤?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤哼鬓,失蹤者是張志新(化名)和其女友劉穎监右,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體异希,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡健盒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了称簿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扣癣。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖憨降,靈堂內(nèi)的尸體忽然破棺而出父虑,到底是詐尸還是另有隱情,我是刑警寧澤授药,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布士嚎,位于F島的核電站,受9級特大地震影響悔叽,放射性物質發(fā)生泄漏莱衩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一娇澎、第九天 我趴在偏房一處隱蔽的房頂上張望笨蚁。 院中可真熱鬧,春花似錦、人聲如沸括细。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奋单。三九已至是掰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間辱匿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工炫彩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留匾七,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓江兢,卻偏偏與公主長得像昨忆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子杉允,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 寫在開始叔磷,講在結尾拢驾。 媽媽,你年輕的時候改基,那么年輕繁疤。 有人愛慕你年輕時,有人傾心那一段秕狰。 你都這么開心的占有著稠腊,或...
    隨風潛夜閱讀 354評論 0 0
  • 先來一張大合照~ 不知不覺架忌,21天的課程就要結束了! 跟著心藍老師學到非常詳細的彩鉛插畫基礎知識我衬,從排線叹放,疊色等等...
    陳少瓊閱讀 749評論 2 2
  • 時間太瘦,指縫太寬 我們抓不住金沙挠羔,攤開手掌 流年已不見许昨。 我將光陰寫在閏年的腳上 踏足 經(jīng)年碎響 響聲叫不醒我 ...
    考拉家的老王子閱讀 398評論 0 0
  • 人無百日好,花無百日紅褥赊。 路邊一晃而過的紫薇顛覆了這個說法糕档。 這種花我們這兒原來沒有這種花,后來引種過來。一到夏天...
    海深深閱讀 685評論 0 1