從API版本升級到4.6以後, Unity支持了async和await語法, 而且根據測試來看, 它運行在主線程裏, 跟通常的C#編譯不大同樣, 這就頗有操做空間了, 先來看看普通C# Console工程和Unity中運行的差異:多線程
1. C# Console框架
using System; namespace AsyncTest { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); Console.WriteLine("Main : " + System.Threading.Thread.CurrentThread.ManagedThreadId); // 1 Test(); Console.ReadLine(); } async static void Test() { await System.Threading.Tasks.Task.Delay(TimeSpan.FromSeconds(5)); Console.WriteLine("Async : " + System.Threading.Thread.CurrentThread.ManagedThreadId); // 4 } } }
運行結果能夠看到運行在不一樣的線程裏面 : 異步
2. Unity async
using UnityEngine; public class AsyncAwaitTest : MonoBehaviour { void Start() { Debug.Log("Main : " + System.Threading.Thread.CurrentThread.ManagedThreadId); // 1 Test(); } async static void Test() { await System.Threading.Tasks.Task.Delay(System.TimeSpan.FromSeconds(5)); Debug.Log("Async : " + System.Threading.Thread.CurrentThread.ManagedThreadId); // 1 } }
運行結果能夠看到運行在主線程裏面 : 函數
這樣的好處是什麼呢? 第一個是它跟協程同樣了, 經過不一樣的await方法返回不一樣的對象實現協程的做用, 我發現它可使用 WaitForSeconds 這些Unity自帶的控制類型, 比較神奇, 看下面測試:性能
using UnityEngine; public class AsyncAwaitTest : MonoBehaviour { void Start() { Debug.Log("Main : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log("Time1 : " + Time.time); Debug.Log("Time2 : " + System.DateTime.Now.ToString("HH:mm:ss fff")); Test(); } async static void Test() { //await System.Threading.Tasks.Task.Delay(System.TimeSpan.FromSeconds(5)); Time.timeScale = 2.0f; await new WaitForSeconds(2.0f); Debug.Log("Async : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log("Time3 : " + Time.time); Debug.Log("Time4 : " + System.DateTime.Now.ToString("HH:mm:ss fff")); } }
運行結果以下:測試
上面的運行在開始時調整了Time.timeScale, 而後等待的時間 WaitForSeconds(2.0) 運行結果也是正確的, 看到遊戲時間過了2秒, 實際時間過了1秒, 也就是說Unity中對await的返回進行了整合, 自帶的YieldInstruction也能被await正確返回. 這樣async方法就能直接當作協程來用了.spa
測試一下多個async嵌套運行的狀況:線程
using UnityEngine; public class AsyncAwaitTest : MonoBehaviour { void Start() { Debug.Log("Main : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log("Time1 : " + Time.time); Debug.Log("Time2 : " + System.DateTime.Now.ToString("HH:mm:ss fff")); Test(); } async void Test() { Time.timeScale = 2.0f; await new WaitForSeconds(2.0f); Debug.Log("Async : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log("Time3 : " + Time.time); Debug.Log("Time4 : " + System.DateTime.Now.ToString("HH:mm:ss fff")); await Test2(); } async System.Threading.Tasks.Task Test2() { await new WaitForSecondsRealtime(2.0f); // Time.timeScale = 2.0f; Debug.Log("Async : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log("Time5 : " + Time.time); Debug.Log("Time6 : " + System.DateTime.Now.ToString("HH:mm:ss fff")); } }
運行結果 : 設計
正確的結果, 由於在Test2中timeScale仍是2, 使用realtime的話就是4秒的遊戲時間.
都是在主線程中運行的, 這樣看來由於async是語言級別的支持, 可能之後就沒有協程什麼事了, 使用async在寫法上也比協程簡單了一點, 咱們試試用協程來寫:
void Start() { Debug.Log("Main : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log("Time1 : " + Time.time); Debug.Log("Time2 : " + System.DateTime.Now.ToString("HH:mm:ss fff")); StartCoroutine(Test()); } IEnumerator Test() { Time.timeScale = 2.0f; yield return new WaitForSeconds(2.0f); Debug.Log("Async : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log("Time3 : " + Time.time); Debug.Log("Time4 : " + System.DateTime.Now.ToString("HH:mm:ss fff")); yield return Test2(); } IEnumerator Test2() { yield return new WaitForSecondsRealtime(2.0f); // Time.timeScale = 2.0f; Debug.Log("Async : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log("Time5 : " + Time.time); Debug.Log("Time6 : " + System.DateTime.Now.ToString("HH:mm:ss fff")); }
差異在StartCoroutine上, 反正我是常常忘了寫它, 而後運行不起來的. 由於沒有什麼好方法測試兩種方案的性能差異, 暫時先拋開性能吧.
而後是 WaitForEndOfFrame 在async是否正確的測試 :
using UnityEngine; public class AsyncAwaitTest : MonoBehaviour { bool update = false; void Start() { Debug.Log("Main : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log("Time1 : " + Time.time); Debug.Log("Time2 : " + System.DateTime.Now.ToString("HH:mm:ss fff")); Test(); } async void Test() { int i = 0; update = true; while(i < 10) { i++; Debug.Log("Async -- " + Time.frameCount); await new WaitForEndOfFrame(); } update = false; } void Update() { if(update) { Debug.Log("Update -- " + Time.frameCount); } } }
能夠看到跟Update函數是交互進行的, 確實async能以YieldInstruction做爲等待邏輯 (更正, 能以Unity已經建立好的YieldInstruction做爲等待邏輯). 這些都驗證了async-await 可以替代協程, 再來測試一個await對異步操做自動返回的類型的:
void Start() { Debug.Log("Main : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log("Time1 : " + Time.time); Debug.Log("Time2 : " + System.DateTime.Now.ToString("HH:mm:ss fff")); var loadPath = Application.streamingAssetsPath + "/mycube"; Load<GameObject>(loadPath, "MyCube", (_prefab) => { var go = GameObject.Instantiate(_prefab); go.name = "MyCube Loaded"; Debug.Log("Time3 : " + System.DateTime.Now.ToString("HH:mm:ss fff")); }); } async void Load<T>(string loadPath, string assetName, System.Action<T> loaded) where T : UnityEngine.Object { AssetBundle assetBundle = await AssetBundle.LoadFromFileAsync(loadPath); UnityEngine.Object asset = await assetBundle.LoadAssetAsync<T>(assetName); loaded.Invoke(asset as T); assetBundle.Unload(false); }
上面的代碼用來讀取一個AssetBundle中的GameObject, 在讀取步驟 await AssetBundle.LoadFromFileAsync(loadPath); 返回的直接就是assetBundle了, 而且在 await assetBundle.LoadAssetAsync<T>(assetName); 直接返回的就是asset(Object)了, 這個可能也是Unity在編譯層面作的改動吧, 因此通過測試正常API都能經過await返回.
這只是基本操做, 其實有更厲害的地方, 它能改變上下文達到跳轉線程的做用. Unity有它本身的同步上下文叫作UnitySynchronizationContext, .NET中叫SynchronizationContext, 由於Unity使用的是.NET標準庫, 因此繼承了Task的ConfigureAwait功能, 它是告訴這個Task能夠運行在其它線程上, 而若是上下文的線程進行了轉換, 若是沒有須要它就不會自動轉回主線程. 測試一下 :
public class EnterWorkThread { public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter() { return Task.Run(() => { }).ConfigureAwait(false).GetAwaiter(); } } void Start() { Debug.Log("Main : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log("Time1 : " + Time.time); Debug.Log("Time2 : " + System.DateTime.Now.ToString("HH:mm:ss fff")); Test(); } async void Test() { Debug.Log("Async1 : " + System.Threading.Thread.CurrentThread.ManagedThreadId); await new EnterWorkThread(); Debug.Log("Async2 : " + System.Threading.Thread.CurrentThread.ManagedThreadId); GameObject.CreatePrimitive(PrimitiveType.Cube); }
能夠看到 await new EnterWorkThread(); 以後當前線程轉換爲了工做線程, 經過這個方式就把上下文轉換到了其它線程裏面. 後面運行的代碼也繼續在新線程中運行.
await 只須要返回對象有GetAwaiter方法便可.
那麼要回到主線程有什麼方法呢? 等待主線程的生命週期便可:
async void Test() { Debug.Log("Async1 : " + System.Threading.Thread.CurrentThread.ManagedThreadId); await new EnterWorkThread(); Debug.Log("Async2 : " + System.Threading.Thread.CurrentThread.ManagedThreadId); await new WaitForEndOfFrame(); Debug.Log("Async3 : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log(GameObject.CreatePrimitive(PrimitiveType.Cube).name); }
看到線程又回到了主線程, 而且調用API沒有問題. 之後寫多線程的代碼能夠很簡單了!!!
(2020.03.06)
PS : 目前本身建立的對象只有繼承於CustomYieldInstruction類的才能做爲awaitable對象, 其它仍是須要按照正常的C#方式來, 而且在執行這個以後必定會回到主線程, 這應該是Unity底層作了強制轉換, 因此纔有了這個寫法的理論支持. 而後這個線程轉換, 在回到主線程的時候都是要等待下一幀的, 跟咱們本身寫的邏輯差很少 :
void OnGUI()
{
if(GUI.Button(new Rect(100, 100, 100, 50), "Test")) { FrameTest(); } } async void FrameTest() { Debug.Log("Main : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log("Frame : " + Time.frameCount); await new EnterWorkThread(); // 工做線程 Debug.Log("WorlThread : " + System.Threading.Thread.CurrentThread.ManagedThreadId); await new EnterMainThread(); Debug.Log("Main : " + System.Threading.Thread.CurrentThread.ManagedThreadId); Debug.Log("Frame : " + Time.frameCount); return; }
PS2 : 在工做線程中進行等待操做, 也須要另外封裝才行, 若是使用Unity的會被強制回到主線程的, 但是即便本身封裝, 也會被強制轉換線程 :
public class WaitTimeWorkThread { private float _time = 0.0f; public WaitTimeWorkThread(float time) { _time = time; } public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter() { return Task.Delay(TimeSpan.FromSeconds(_time)).ConfigureAwait(false).GetAwaiter(); } } async void FrameTest() {
Debug.Log("Main : " + System.Threading.Thread.CurrentThread.ManagedThreadId);
await new EnterWorkThread(); // 工做線程1
Debug.Log("EnterWorkThread : " + System.Threading.Thread.CurrentThread.ManagedThreadId);
await new WaitTimeWorkThread(1.0f); // 工做線程2
Debug.Log("WaitTimeWorkThread : " + System.Threading.Thread.CurrentThread.ManagedThreadId);
}
結果很不理想, 在線程中仍是被轉換了線程 :
若是是多重嵌套的邏輯, 隨着上下文轉換的開銷增長, 很難說性能影響的大小, 而且全部調用都要注意線程問題, 有些邏輯自帶線程轉換的, 就比較麻煩了, 雖然跟普通多線程比較方便了不少, 但是跟Job系統比起來又弱爆了, 各有各的好吧.
補充一下額外的相關信息, 一個普通協程它是能夠被中止的, 經過關閉運行這個協程的GameObject, 或者是調用StopCoroutine方法, 咱們使用async方法的話, 就很sucks了, 由於語言自己就沒有提供中止Task的方法, 測試了它提供的CancellationToken簡直就是個智障設計, 徹底沒有實際意義. 看看微軟本身提供的例子 :
static async Task Main()
{
var tokenSource2 = new CancellationTokenSource(); CancellationToken ct = tokenSource2.Token; var task = Task.Run(() => { ct.ThrowIfCancellationRequested(); bool moreToDo = true; while (moreToDo) { if (ct.IsCancellationRequested) { ct.ThrowIfCancellationRequested(); }
} }, tokenSource2.Token); // Pass same token to Task.Run. tokenSource2.Cancel(); try { await task; } catch (OperationCanceledException e) { Console.WriteLine($"{nameof(OperationCanceledException)} thrown with message: {e.Message}"); } finally { tokenSource2.Dispose(); } Console.ReadKey(); }
除了一句MDZZ以外還能說什麼呢, 在全部代碼前添加異常拋出嗎? 在全部循環中本身添加嗎? 簡直弱爆了啊.
若是使用殺線程的方式不知道是否可行, 由於在這裏的async模式下, 咱們是能夠不斷轉換線程的, 主線程的話怎麼辦? 不能殺線程也不能中止. 還有它進入工做線程的時候怎樣記錄線程ID也是個問題......
無論怎樣, 它提供了另一種協程或多線程的方式, 加上ECS on Job, 項目中就能夠有知足各類需求的多線程框架了.