ET框架很多地方都用到了異步,例如資源加載、AI、Actor模型等等。ET框架對C#的異步操作進行了一定程度的封裝和改造,有一些特點:
顯式的或者說強調了使用C#異步實現協程機制(其實C#的異步編程天生就能實現這種用法)強制單線程異步沒有使用C#庫的Task,自己實現了ETTask等類實現了協程鎖為了更好的理解下面的內容,推薦先看一下之前寫的這兩篇文章:
關于異步對CallbackHell的優化 跳轉鏈接:《Lua CallbackHell優化》關于C#異步編程介紹和底層實現(最好看下,不然下面有些內容不太好理解) 跳轉鏈接:《C# 異步編程async/await》ETTaskC# 的異步函數有三個返回值(現在好像.NET7又多了一個ValueTask):Task,Task
【資料圖】
ETTask添加了一些特性:
支持對象池顯式強調協程[DebuggerHidden]private async ETVoid InnerCoroutine(){ await this;}[DebuggerHidden]public void Coroutine(){ InnerCoroutine().Coroutine();}可以看到這里的所謂協程Coroutine,其實等效于 await task,只是平平無奇的異步調用罷了
異常消息打印同步上線文 SynchronizationContextC#異步編程在大多數情況下會使用多線程,ET的異步操作例如定時器等,使用多線程的開銷相比較大,且ET框架是多進程,性能是分攤到多個進程中。所以ET使用了單線程的異步。
ThreadSynchronizationContext繼承自SynchronizationContext,在構造初始化是會把自身設為當前SynchronizationContext.Current,重寫了Post(異步消息分派到同步上下文)方法,來改寫異步消息的分派到當前線程(就是進入隊列)。
而異步函數在執行時,會獲取當前上下文(__builder.AwaitUnsafeOnCompleted方法會調用GetCompletionAction,內部調用ExecutionContext.FastCapture(),這個方法內部捕獲SynchronizationContext,感興趣可以關鍵詞搜索下)
public class ThreadSynchronizationContext : SynchronizationContext{ // 線程同步隊列,發送接收socket回調都放到該隊列,由poll線程統一執行 private readonly ConcurrentQueue queue = new ConcurrentQueue(); private Action a; public void Update() { while (true) { if (!this.queue.TryDequeue(out a)) { return; } try { a(); } catch (Exception e) { Log.Error(e); } } } public override void Post(SendOrPostCallback callback, object state) { this.Post(() => callback(state)); } public void Post(Action action) { this.queue.Enqueue(action); }}public class MainThreadSynchronizationContext: Singleton, ISingletonUpdate{ private readonly ThreadSynchronizationContext threadSynchronizationContext = new ThreadSynchronizationContext(); public MainThreadSynchronizationContext() { SynchronizationContext.SetSynchronizationContext(this.threadSynchronizationContext); } public void Update() { this.threadSynchronizationContext.Update(); } public void Post(SendOrPostCallback callback, object state) { this.Post(() => callback(state)); } public void Post(Action action) { this.threadSynchronizationContext.Post(action); }}// MainThreadSynchronizationContext.Instance.Update()Game.Update(); ThreadSynchronizationContex由包裹的MainThreadSynchronizationContext驅動更新,MainThreadSynchronizationContext是個單件,由外面驅動。更新Update方法會把隊列里的委托取出執行。
SynchronizationContext假設有兩個線程,一個UI線程,一個后臺線程,一個業務先在后臺線程計算數據,然后在UI線程中刷新顯示數據,顯然不同的線程其上下文環境是不同的,兩個線程的通信可以使用SynchronizationContext完成。SynchronizationContext官方文檔 https://learn.microsoft.com/zh-CN/dotnet/api/system.threading.synchronizationcontext?view=netcore-3.0
協程鎖多線程編程,對公共資源的訪問要加鎖,以保證數據訪問的安全。類似的,在ET的異步編程中,從雖然上文中可以了解到ET的異步其實是單線程的,從代碼運行的層面其實是一個線程以某種順序處理一個個的任務,但是這種“順序”并不可控。ET這里的協程鎖其實就是使用某個key,對所有用這個key包裹的代碼段推入一個隊列,只有前面的代碼段執行結束才能執行后面的代碼。
這看起來和C#平時用的lock(object),其實只是用法上比較像,其實在實現細節是有根本的差距的:簡單來說。ET實現的協程鎖是一種用戶態的鎖,不會造成內核態/用戶態的切換。而lock是一種C#語法糖,在編譯時其實是通過Monitor監視器實現的,會涉及到內核轉換。一個線程上可能會運行成百上千個協程,如果這個線程被掛起,那么有可能造成很多協程Delay,可能造成災難性的后果。
結構類圖:時序圖:結合ET工程官方的一個用法:
public static async ETTask Query(this DBComponent self, long id, string collection = null) where T : Entity{ using (await CoroutineLockComponent.Instance.Wait(CoroutineLockType.DB, id % DBComponent.TaskCount)) { IAsyncCursor cursor = await self.GetCollection(collection).FindAsync(d => d.Id == id); return await cursor.FirstOrDefaultAsync(); }} 可以看到協程鎖是被using包裹的,即{}包裹的代碼塊運行結束,協程鎖會被dispose。先來看當第一次調用Wait時會直接返回,當第一次的鎖沒有被dispose時,后面獲取鎖時會進入隊列。當前面的鎖被dispose時,會通知隊列中后面一個鎖在下一次Update時被Notify,SetResult獲取到鎖,其所屬的代碼段得以執行。
關鍵詞: