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


在上一回合談到,客戶端應用程序的所有操作都在主線程上進行,所以一些比較耗時的操作可以在異步線程上去進行,充分利用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上,點擊此了解


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM