在上一回合談到,客戶端應用程式的所有操作都在主執行緒上進行,所以一些比較耗時的操作可以在非同步執行緒上去進行,充分利用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不讀快取直接把最新的值返回。所以_shouldStop
被volatile
修飾。
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混合使用
到目前為止,相信你對Coroutine
和Thread
有清楚的認識,但它們並不是互斥的,可以混合使用,比如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實際上就是IEnumerator
和yield
這兩個語法糖讓我們很難理解其中的奧祕,推薦使用反編譯工具去檢視,相信你會豁然開朗。
原始碼託管在Github上,點選此瞭解
歡迎關注我的公眾號: