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

木宛城主發表於2017-08-18

在上一回合談到,客戶端應用程式的所有操作都在主執行緒上進行,所以一些比較耗時的操作可以在非同步執行緒上去進行,充分利用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上,點選此瞭解

歡迎關注我的公眾號:

相關文章