原理對比
Coroutine | UniTask | |
本質 | IEnumerator 的協作調度器 | async/await 狀態機(IAsyncStateMachine) |
調度方式 | Unity 內部調用 MoveNext() | 自建 PlayerLoopRunner 控制狀態推進 |
內存管理 | 引用類型,頻繁分配 GC | 結構體 UniTask,低 GC 壓力 |
多線程支持 | 主線程限制 | 可結合多線程(但默認仍在主線程) |
工具組合能力 | 弱 | 強(如 WhenAll, WithCancellation) |
調用方式
協程
1.不使用StartCoroutine調用時,可通過編譯,但是無法啟動,協程進不去
2.使用StartCoroutine可正確執行協程邏輯,正常執行等待,對當前函數體無要求
3.可使用yield return等待協程執行完畢,需要當前函數體有IEnumerator關鍵字標識
4.可使用await關鍵字等待協程執行完畢,需要當前函數體有async關鍵字標識
示例代碼如下
void CorTest()
{Test();Debug.Log("Coroutine 3");StartCoroutine(Test());Debug.Log("Coroutine 4");
}IEnumerator Test()
{Debug.Log("Coroutine 1");yield return new WaitForSeconds(1.1f);Debug.Log("Coroutine 2");
}
當調用CorTest后,輸出結果為
可以看到,先輸出了"Coroutine 3",而沒有輸出"Coroutine 1",表示沒有使用StartCoroutine啟動時,協程是進不去的。使用了StartCoroutine后,"Coroutine 2"在"Coroutine 1"及"Coroutine 4"一秒后輸出。await關鍵字情況同下面UniTask調用
UniTask
1.UniTask無論是否使用await關鍵字,都可以正確進入邏輯,正常執行等待,對當前函數體無要求
2.可使用await等待UniTask執行完畢,需要當前函數體有async關鍵字標識
示例代碼如下
async void TaskTest()
{Test2();Debug.Log("UniTask 3");await Test2();Debug.Log("UniTask 4");
}async UniTask Test2()
{Debug.Log("UniTask 1");await UniTask.Delay(1100);Debug.Log("UniTask 2");
}
當調用TaskTest后,輸出結果為
可以看到,兩次調用Test2均正常進入,且正常執行了等待邏輯,兩次"UniTask 2"輸出均在"UniTask 1"后,而"UniTask 4"輸出也是在"UniTask 2"后執行的。函數體如果沒有async關鍵字時,內部是無法使用await的,編譯不通過。
性能測試
yield return null? VS?UniTask.Yield()
測試代碼
public int times = 1000;void CorProTest()
{StartCoroutine(CorProEnum());
}IEnumerator CorProEnum()
{for (int i = 0; i < times; i++){yield return null;}
}void UniTaskProTest()
{UniTaskProTask();
}async UniTask UniTaskProTask()
{for (int i = 0; i < times; i++){await UniTask.Yield();}
}
由于無法抓取一段時間內的純Profiler數據,所以只取一幀的數據,每幀數據都是一致的。
可以看到,兩個對GC都沒有影響,因為協程本身并沒有新建對象,所以不存在分配內存。可以理解成等價的。
?yield return new WaitForSeconds VS?UniTask.Delay
測試代碼
public int times = 1000;void CorProTest()
{StartCoroutine(CorProEnum());
}IEnumerator CorProEnum()
{for (int i = 0; i < times; i++){yield return new WaitForSeconds(0.01f);}
}void UniTaskProTest()
{UniTaskProTask();
}async UniTask UniTaskProTask()
{for (int i = 0; i < times; i++){await UniTask.Delay(10);}
}
同上,抓取某一幀的數據
?可以看到,調用yield return new WaitForSeconds(0.01f)時,有20B的內存分配,這是因為創建了引用對象WaitForSeconds,所以必定會有內存分配。調用await UniTask.Delay(10)沒有內存分配,是因為UniTask內部使用的是結構體,而不是類。
yield return new WaitUntil VS?UniTask.WaitUntil
測試代碼
public int times = 1000;void CorProTest()
{StartCoroutine(CorProEnum());
}IEnumerator CorProEnum()
{bool value = true;for (int i = 0; i < times; i++){yield return new WaitUntil(() => value);}
}void UniTaskProTest()
{UniTaskProTask();
}async UniTask UniTaskProTask()
{bool value = true;for (int i = 0; i < times; i++){await UniTask.WaitUntil(() => value);}
}
同上,抓取某一幀數據
可以看到 ,調用yield return new WaitUntil時,有24B的內存分配,這是因為創建了引用對象WaitUntil,所以必定會有內存分配。調用await UniTask.WaitUntil沒有內存分配,是因為UniTask內部使用的是結構體,而不是類。
總結
????????測試了這三種常用的用法,可以看到,協程除了null沒有GC產生(因為沒有創建對象)外,其他兩種用戶均產生了GC,只是量比較小,而UniTask三種用法都沒有GC產生。
????????如果只考慮GC方面的差異,在項目使用過程中,如果量比較大,使用比較頻繁,建議使用UniTask。而對于一般用量來講,差距可以忽略不計。而GC是可以使用對象池來優化的,可以一定程度上降低GC的分配。對象池參考另一篇博客從CPU緩存出發對引用池進行優化。
? ? ? ? 然而,最終選擇使用哪一種,需要結合其他情況考慮,協程使用起來比較方便,而UniTask也有一些比較好的功能,比如UniTask支持帶返回值的異步,封裝了多任務同時進行、等待以及其他功能。