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

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

Thread 初步認識

雖然這不是什麼難點,但我以爲仍是有必要提一下多線程編程幾個值得注意的事項:github

  • 線程啓動

在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上,點擊此瞭解

歡迎關注個人公衆號:

相關文章
相關標籤/搜索