ET框架很多地方都用到了異步,例如資源加載、AI、Actor模型等等。ET框架對C#的異步操作進(jìn)行了一定程度的封裝和改造,有一些特點(diǎn):
顯式的或者說強(qiáng)調(diào)了使用C#異步實(shí)現(xiàn)協(xié)程機(jī)制(其實(shí)C#的異步編程天生就能實(shí)現(xiàn)這種用法)強(qiáng)制單線程異步?jīng)]有使用C#庫的Task,自己實(shí)現(xiàn)了ETTask等類實(shí)現(xiàn)了協(xié)程鎖為了更好的理解下面的內(nèi)容,推薦先看一下之前寫的這兩篇文章:
關(guān)于異步對CallbackHell的優(yōu)化 跳轉(zhuǎn)鏈接:《Lua CallbackHell優(yōu)化》關(guān)于C#異步編程介紹和底層實(shí)現(xiàn)(最好看下,不然下面有些內(nèi)容不太好理解) 跳轉(zhuǎn)鏈接:《C# 異步編程async/await》ETTaskC# 的異步函數(shù)有三個(gè)返回值(現(xiàn)在好像.NET7又多了一個(gè)ValueTask):Task,Task
【資料圖】
ETTask添加了一些特性:
支持對象池顯式強(qiáng)調(diào)協(xié)程[DebuggerHidden]private async ETVoid InnerCoroutine(){ await this;}[DebuggerHidden]public void Coroutine(){ InnerCoroutine().Coroutine();}
可以看到這里的所謂協(xié)程Coroutine,其實(shí)等效于 await task,只是平平無奇的異步調(diào)用罷了
異常消息打印同步上線文 SynchronizationContextC#異步編程在大多數(shù)情況下會(huì)使用多線程,ET的異步操作例如定時(shí)器等,使用多線程的開銷相比較大,且ET框架是多進(jìn)程,性能是分?jǐn)偟蕉鄠€(gè)進(jìn)程中。所以ET使用了單線程的異步。
ThreadSynchronizationContext繼承自SynchronizationContext,在構(gòu)造初始化是會(huì)把自身設(shè)為當(dāng)前SynchronizationContext.Current,重寫了Post(異步消息分派到同步上下文)方法,來改寫異步消息的分派到當(dāng)前線程(就是進(jìn)入隊(duì)列)。
而異步函數(shù)在執(zhí)行時(shí),會(huì)獲取當(dāng)前上下文(__builder.AwaitUnsafeOnCompleted方法會(huì)調(diào)用GetCompletionAction,內(nèi)部調(diào)用ExecutionContext.FastCapture(),這個(gè)方法內(nèi)部捕獲SynchronizationContext,感興趣可以關(guān)鍵詞搜索下)
public class ThreadSynchronizationContext : SynchronizationContext{ // 線程同步隊(duì)列,發(fā)送接收socket回調(diào)都放到該隊(duì)列,由poll線程統(tǒng)一執(zhí)行 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驅(qū)動(dòng)更新,MainThreadSynchronizationContext是個(gè)單件,由外面驅(qū)動(dòng)。更新Update方法會(huì)把隊(duì)列里的委托取出執(zhí)行。
SynchronizationContext假設(shè)有兩個(gè)線程,一個(gè)UI線程,一個(gè)后臺線程,一個(gè)業(yè)務(wù)先在后臺線程計(jì)算數(shù)據(jù),然后在UI線程中刷新顯示數(shù)據(jù),顯然不同的線程其上下文環(huán)境是不同的,兩個(gè)線程的通信可以使用SynchronizationContext完成。SynchronizationContext官方文檔 https://learn.microsoft.com/zh-CN/dotnet/api/system.threading.synchronizationcontext?view=netcore-3.0
協(xié)程鎖多線程編程,對公共資源的訪問要加鎖,以保證數(shù)據(jù)訪問的安全。類似的,在ET的異步編程中,從雖然上文中可以了解到ET的異步其實(shí)是單線程的,從代碼運(yùn)行的層面其實(shí)是一個(gè)線程以某種順序處理一個(gè)個(gè)的任務(wù),但是這種“順序”并不可控。ET這里的協(xié)程鎖其實(shí)就是使用某個(gè)key,對所有用這個(gè)key包裹的代碼段推入一個(gè)隊(duì)列,只有前面的代碼段執(zhí)行結(jié)束才能執(zhí)行后面的代碼。
這看起來和C#平時(shí)用的lock(object),其實(shí)只是用法上比較像,其實(shí)在實(shí)現(xiàn)細(xì)節(jié)是有根本的差距的:簡單來說。ET實(shí)現(xiàn)的協(xié)程鎖是一種用戶態(tài)的鎖,不會(huì)造成內(nèi)核態(tài)/用戶態(tài)的切換。而lock是一種C#語法糖,在編譯時(shí)其實(shí)是通過Monitor監(jiān)視器實(shí)現(xiàn)的,會(huì)涉及到內(nèi)核轉(zhuǎn)換。一個(gè)線程上可能會(huì)運(yùn)行成百上千個(gè)協(xié)程,如果這個(gè)線程被掛起,那么有可能造成很多協(xié)程Delay,可能造成災(zāi)難性的后果。
結(jié)構(gòu)類圖:時(shí)序圖:結(jié)合ET工程官方的一個(gè)用法:
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(); }}
可以看到協(xié)程鎖是被using包裹的,即{}包裹的代碼塊運(yùn)行結(jié)束,協(xié)程鎖會(huì)被dispose。先來看當(dāng)?shù)谝淮握{(diào)用Wait時(shí)會(huì)直接返回,當(dāng)?shù)谝淮蔚逆i沒有被dispose時(shí),后面獲取鎖時(shí)會(huì)進(jìn)入隊(duì)列。當(dāng)前面的鎖被dispose時(shí),會(huì)通知隊(duì)列中后面一個(gè)鎖在下一次Update時(shí)被Notify,SetResult獲取到鎖,其所屬的代碼段得以執(zhí)行。
關(guān)鍵詞: