Unity應用架構設計(10)——繞不開的協程和多線程(Part 1)

来源:http://www.cnblogs.com/OceanEyes/archive/2017/05/24/coroutine_vs_threading.html
-Advertisement-
Play Games

在進入本章主題之前,我們必須要瞭解客戶端應用程式都是 單線程模型 ,即只有一個主線程(Main Thread),或者叫做UI線程,即所有的UI控制項的創建和操作都是在主線程上完成的。而伺服器端應用程式,也就是我們常見的Web應用程式往往是多線程的,故用戶A訪問勢必不會影響用戶B的訪問過程。所以對於We ...


在進入本章主題之前,我們必須要瞭解客戶端應用程式都是單線程模型,即只有一個主線程(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上,點擊此瞭解


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 本文是一篇關於《Effective Python》書中一節的學習筆記,記錄了示例代碼和思路。 如果函數要產生一系列結果,那麼最簡單的做法就是把這些結果都放在一個列表裡返回。 比如我們要查出字元串中每個詞的首字母在整串字元串中的位置: 該函數的使用: 這個函數思路很明瞭,但存在的問題在於代碼擁擠、冗餘 ...
  • 題目描述 在有向圖G 中,每條邊的長度均為1 ,現給定起點和終點,請你在圖中找一條從起點到終點的路徑,該路徑滿足以下條件: 1 .路徑上的所有點的出邊所指向的點都直接或間接與終點連通。 2 .在滿足條件1 的情況下使路徑最短。 註意:圖G 中可能存在重邊和自環,題目保證終點沒有出邊。 請你輸出符合條 ...
  • 很久之前就用到過這個函數,只不不過是簡單的用用而已並沒有做太深入的研究 今天在翻閱別人博客時看到了對array_merge的一些使用心得,故此自己來進行一次總結。 array_merge是將一個或者多個數組進行合併。 這個函數多用於在從資料庫中取出的結果集的合併操作。 參數配置也很簡單array_m ...
  • 堆棧: 具有一定操作約束的線性表,只在一端(棧頂)做出棧和入棧(先進後出) 棧的順序存儲實現: 棧的鏈式存儲解決(棧頂在鏈棧的棧頂): 表達式求值問題 中綴表達式:運算符號位於兩個運算數之間。如:a+b*c-d/e 尾碼表達式:運算符號位於兩個運算數之後。如:abc*+de/- 中綴表達式轉換為尾碼 ...
  • http://www.blogjava.net/fhtdy2004/archive/2009/05/03/268720.html 配置文件的詳細介紹 ...
  • 來自於:http://www.jb51.net/article/38051.htm http://blog.csdn.net/Neil_Wesley/article/details/51484026 題目:猴子吃桃問題:猴子第一天摘下若幹個桃子,當即吃了一半,還不癮,又多吃了一個 第二天早上又將剩下 ...
  • 《電腦科學叢書:Java編程思想(第4版)》贏得了全球程式員的廣泛贊譽,即使是晦澀的概念,在BruceEckel的文字親和力和小而直接的編程示例面前也會化解於無形。從Java的基礎語法到高級特性(深入的面向對象概念、多線程、自動項目構建、單元測試和調試等),本書都能逐步指導你輕鬆掌握。 從《電腦 ...
  • 一丶查看JDK的引用路徑是否報錯。移除原來jdk,換為本機的jdk即可。 選擇jre system library,一般預設本地jdk,直接Finnish就好了。 二丶web工程看是否引入了web App library,沒有的話,項目右鍵properties >java build path 右側 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...