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

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

在上一回合談到,客戶端應用程式的所有操作都在主線程上進行,所以一些比較耗時的操作可以在非同步線程上去進行,充分利用CPU的性能來達到程式的最佳性能。對於Unity而言,又提供了另外一種『非同步』的概念,就是協程( ),通過反編譯,它本質上還是在主線程上的優化手段,並不屬於真正的多線程( )。那麼問題來了 ...


在上一回合談到,客戶端應用程式的所有操作都在主線程上進行,所以一些比較耗時的操作可以在非同步線程上去進行,充分利用CPU的性能來達到程式的最佳性能。對於Unity而言,又提供了另外一種『非同步』的概念,就是協程(Coroutine),通過反編譯,它本質上還是在主線程上的優化手段,並不屬於真正的多線程(Thread)。那麼問題來了,怎樣在Unity中使用多線程呢?

Thread 初步認識

雖然這不是什麼難點,但我覺得還是有必要提一下多線程編程幾個值得註意的事項:

  • 線程啟動

在Unity中創建一個非同步線程是非常簡單的,直接使用類System.Threading.Thread就可以創建一個線程,線程啟動之後畢竟要幫我們去完成某件事情。在編程領域,這件事就可以描述了一個方法,所以需要在構造函數中傳入一個方法的名稱。

Worker workerObject = new Worker();
Thread workerThread = new Thread(workerObject.DoWork)
workerThread.Start();
  • 線程終止

線程啟動很簡單,那麼線程終止呢,是不是調用Abort方法。不是,雖然Thread對象提供了Abort方法,但並不推薦使用它,因為它並不會馬上停止,如果涉及非托管代碼的調用,還需要等待非托管代碼的處理結果。

一般停止線程的方法是為線程設定一個條件變數,線上程的執行方法里設定一個迴圈,並以這個變數為判斷條件,如果為false則跳出迴圈,線程結束。

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來到達線程的安全退出。

  • 共用數據處理

多線程最麻煩的一點就是共用數據的處理了,想象一下A,B兩個線程同一時刻處理一個變數,它最終的值到底是什麼。所以一般需要使用lock,但C#提供了另一個關鍵字volatile,告訴CPU不讀緩存直接把最新的值返回。所以_shouldStopvolatile修飾。

Dispatcher的引入

是不是覺得多線程好簡單,好像也沒想象的那麼複雜,當你愉快的在多線程中訪問UI控制項時,Duang~~~,一個錯誤告訴你,不能在非同步線程訪問UI控制項。這是肯定的,跨線程訪問UI控制項是不安全的,理應被禁止。那怎麼辦呢?

如果你有其他客戶端的開發經驗,比如iOS或者WPF經驗,肯定知道Dispatcher。Dispatcher翻譯過來就是調度員的意思,簡單理解就是每個線程都有唯一的調度員,那麼主線程就有主線程的調度員,實際上我們的代碼最終也是交給調度員去執行,所以要去訪問UI線程上的控制項,我們可以間接的向調度員發出命令。

所以在WPF中,跨線程訪問UI控制項一般的寫法如下:

Thread thread=new Thread(()=>{
    this.Dispatcher.Invoke(()=>{
        //UI
        this.textBox.text=...
        this.progressBar.value=...
    });
});

嗯~ o( ̄▽ ̄)o,不錯,但尷尬的是Unity沒有提供Dispatcher啊!

對,但我們可以自己實現,把握住幾個關鍵點:

  • 自己的Dispatcher一定是一個MonoBehaviour,因為訪問UI控制項需要在主線程上
  • 什麼時候去更新呢,考慮生產者-消費者模式,有任務來了,我就是更新到UI上
  • 在Unity中有這麼個方法可以輪詢是不是有任務要更新,那就是Update方法,每一幀會執行

所以自定義的UnityDispatcher提供一個BeginInvoke方法,並接送一個Action

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

這是一個生產者,向隊列里添加需要處理的Action。有了生產者之後,還需要消費者,Unity中的Update就是一個消費者,每一幀都會執行,所以如果隊列里有任務,它就執行

 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,好處是它以原子的操作來執行並且還不會阻塞線程,因為主線程本身任務繁重,所以我不推薦使用lock

Coroutine和MultiThreading混合使用

到目前為止,相信你對CoroutineThread有清楚的認識,但它們並不是互斥的,可以混合使用,比如Coroutine等待非同步線程返回結果,假設非同步線程里執行的是非常複雜的AI操作,這顯然放在主線程會非常繁重。

由於篇幅有限,我不貼完整代碼了,只分析其中最核心思路:
Thread中有一個WaitFor方法,它每一幀都會詢問非同步任務是否完成:

public bool Update(){
    if(_isDown){
        OnFinished ();
        return true;

    }
    return false;
}
public IEnumerator WaitFor(){
    while(!Update()){
        //暫停協同程式,下一幀再繼續往下執行
        yield return null;
    }
}

那麼在某一個UI線程中,等待非同步線程的結果,註意利用StartCouroutine,此等待並非阻塞線程,相信你已經它內部的機制了。

void Start(){

    Debug.Log("Main Thread :"+Thread.CurrentThread.ManagedThreadId+" work!");
    StartCoroutine (Move());
}

IEnumerator Move()
{
    pinkRect.transform.DOLocalMoveX(250, 1.0f);
    yield return new WaitForSeconds(1);
    pinkRect.transform.DOLocalMoveY(-150, 2);
    yield return new WaitForSeconds(2);
    //AI操作,陷入深思,在非同步線程執行,GreenRect不會卡頓
    job.Start();
    yield return StartCoroutine (job.WaitFor());
    pinkRect.transform.DOLocalMoveY(150, 2);

}

小結

這兩篇文章為大家介紹了怎樣在Unity中使用協程和多線程,多線程其實不難,但同步數據是最麻煩的。Coroutine實際上就是IEnumeratoryield這兩個語法糖讓我們很難理解其中的奧秘,推薦使用反編譯工具去查看,相信你會豁然開朗。
源代碼托管在Github上,點擊此瞭解


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

-Advertisement-
Play Games
更多相關文章
  • 本書通篇都是以情景對話的形式,用多個小故事或編程示例來組織講解GoF(設計模式的經典名著——Design Patterns:Elements of Reusable Object-Oriented Software,中譯本名為《設計模式——可復用面向對象軟體的基礎》的四位作者Erich Gamma、 ...
  • 控制套接字的行為(如修改緩衝區的大小)。 int getsockopt(int sockfd,int level,int optname,void *optval,socklen_t *optlen); int setsockopt(int sockfd,int level,int optname, ...
  • 作者Brett Slatkin是 Google公司高級軟體工程師。他是Google消費者調查項目的工程主管及聯合創始人,曾從事Google App Engine的Python基礎架構工作,並利用Python來管理眾多的Google伺服器。Slatkin也是PubSubHubbub協議的聯合創始人,還 ...
  • “註解”這個詞,可謂是在Java編程中出鏡率比較高,而且也是一個老生常談的話題。我們之前在聊Spring相關的東西時,註解是無處不在,之前我們簡單的聊過一些“註解”的相關內容,比如在Spring中是如何進行“註解”組合的。因為註解在Java編程中還是比較重要的,所以我們今天的博客就把註解的東西給系統 ...
  • 問題:Firemonkey Android 平臺顯示斜粗體文字時,文字右方會有顯示不全的問題。 修正代碼: 請將 FMX.FontGlyphs.Android.pas 複製到自己的工程目錄下,再修改如下代碼: 修正效果: ...
  • 數據源連接池配置 ...
  • 列印流 在整個 包中,列印流是輸出信息最方便的類,主要包含 位元組列印流 ( )和 字元列印流 ( )。列印流提供了非常方便的列印功能,可以列印任何的數據類型,例如:小數、整數、字元串等等,相對於前面學習的幾個文件的操作來說,這裡的列印流是最簡便的一個類了 PrintStream 主要功能是格式化的將 ...
  • 記憶體操作流 之前的所有的流操作都是針對文件的,但是有時候只是想要實現數據間轉換,此時如果我們想要創建一個文件然後再刪除文件,那樣顯得有點麻煩,因此此時的記憶體操作流就顯得很適合這類的操作,因為它只是在記憶體中存儲,並不會真正的創建文件,記憶體操作流涉及的兩個類是 ,`ByteArrayOutputStre ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...