從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
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方法就能直接當做協程來用了.
測驗一下多個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, 專案中就可以有滿足各種需求的多執行緒框架了.
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/27547.html
標籤:其他
上一篇:為什么手機一連,埠立刻完蛋?
下一篇:云計算和云服務的區別
