出來混總是要還的
最近在準備記錄一個.NET Go核心能力的深度對比, 關於.NET/Go的異步實現總感覺沒敲到點上。
async/await是.NET界老生常談的話題,每至於此,狀態機又是必聊的話題,但是狀態機又是比較晦澀難懂的話題。
[一線碼農大佬]在博客園2020年寫的《await,async 我要把它翻個底朝天,這回你總該明白了吧》手把手實現了異步狀態機,這篇文章很是經典, 但是評論區很多人還是在吐槽看不懂, 我也看的不是很懂。
以我淺薄的推測:
- 一線大佬的知識體系太寬太深,有的驗證點在文字之外,需要我們自己去確認。
- 有些內容太細節,挖的太深,出不來。
- 很多人不熟悉狀態機設計模式, 導致看大佬文章,知其然不知其所以然。
我以前用Go語言演示了狀態機: 我是狀態機,有一顆永遠騷動的機器引擎, 當時有粉絲留言讓用.NET 實現狀態機, 這篇文章也算是對粉絲的喊話。
狀態機:一顆永遠騷動的機器引擎
狀態機是一種行爲設計模式,它允許對象在其內部狀態改變時改變其行爲。看起來好像對象改變了它的類。
請仔細理解上面每一個字。
我們以自動售貨機爲例,爲簡化演示,我們假設自動售貨機只有1種商品, 故自動售貨機有itemCount
、itemPrice
2個屬性
不考慮動作的前後相關性,自動售貨機對外暴露4種行爲:
- 給自動售貨機加貨
addItem
- 選擇商品
requestItem
- 付錢
insertMoney
- 出貨
dispenseItem
重點來了,當發生某種行爲,自動售貨機會進入如下4種狀態之一, 並據此狀態做出特定動作, 之後進入另外一種狀態.....
- 有商品
hasItem
- 無商品
noItem
- 已經選好商品
itemRequested
- 已付錢
hasMoney
當對象可能處於多種不同的狀態之一、根據傳入的動作更改當前的狀態, 繼續接受後續動作,狀態再次發生變化.....
這樣的模式類比於機器引擎,週而復始的工作和狀態轉化,這也是狀態機的定語叫“機Machine”的原因。
有了以上思路,我們嘗試溝通UML 僞代碼
狀態機設計模式的僞代碼實現:
- 所謂的機器Machine維護了狀態切換的上下文
- 機器對外暴露的行爲,驅動機器的狀態變更
- 機器到達特定的狀態 只具備特定的行爲,其他行爲是不被允許的
Go版本的售貨機(狀態機設計模式)的源碼,請參見原文https://www.cnblogs.com/JulianHuang/p/15304184.html。
async/await貼臉開大
還是以一線碼農大佬的異步下載爲例:
編譯器詞法分析定位到async/await語法糖,就會爲開發者生成狀態機代碼。
一個新出爐的狀態機包含如下屬性 :
(1) 初始化的狀態機,以async所在的函數名命名,示例狀態機爲<GetResult>d__1
;
(2)車鑰匙啓動狀態機之後,立馬返回,這正是異步編程
的內涵。
一個簡單的、成功的狀態機轉化如圖:
1. 初始狀態
- state= -1;
- Start狀態機; 即時返回。
Program.<GetResult>d__1 stateMachine = new Program.<GetResult>d__1();
stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start<Program.<GetResult>d__1>(ref stateMachine);
return stateMachine.<>t__builder.Task;
車鑰匙Start,內部實際是執行MoveNext
方法,
該方法會設置異步任務的TaskAwaiter
對象, 緊接着stateMachine進入新的狀態。
2. 異步任務未完成狀態
- 切換到state = 0
- 調用
AwaitUnsafeOnCompleted()
向底層註冊回調, 2個入參- 回調參數1:異步結果
TaskAwaiter<TResult>
- 回調參數2: 當前狀態機
- 回調參數1:異步結果
int num1 = this.<>1__state;
if (num1 != 0)
{
this.<client>5__1 = new WebClient();
awaiter = this.<client>5__1.DownloadStringTaskAsync(new Uri("http://cnblogs.com")).GetAwaiter();
if (!awaiter.IsCompleted)
{
this.<>1__state = num2 = 0;
this.<>u__1 = awaiter;
Program.<GetResult>d__1 stateMachine = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, Program.<GetResult>d__1>(ref awaiter, ref stateMachine);
return;
}
}
IO數據就緒,會在IO線程執行回調方法GetCompletionAction
,利用入參2:狀態機,再次執行狀態機的MoveNext
方法, 進入新的狀態
3. 異步結果就緒狀態
- 切換到state = -1;
- taskAwaiter獲取異步任務結果;
- 執行後繼代碼;
else
{
awaiter = this.<>u__1;
this.<>u__1 = new TaskAwaiter<string>();
this.<>1__state = num2 = -1;
}
this.<>s__3 = awaiter.GetResult();
this.<content>5__2 = this.<>s__3;
this.<>s__3 = (string) null;
content52 = this.<content>5__2; // 後繼代碼段
4. 狀態機終止狀態
- 切換到state =-2;
- 設置狀態機最終返回值;
this.<>1__state = -2;
this.<client>5__1 = (WebClient) null;
this.<content>5__2 = (string) null;
this.<>t__builder.SetResult(content52);
以上四個狀態的貼臉源碼均截取自ILspy反編譯結果,讀者可將代碼和狀態輪轉圖對比。
一線碼農大佬講: 一個簡單成功的async/await狀態機會經歷 2次MoveNext
動作 ,我是認同的。
一次是狀態機啓動,主動切換狀態;
第二次是IO數據就緒,回調函數會執行原狀態機的MoveNext
方法, 這個是在註冊回調的時候確定的。
下面是第二次MoveNext
方法的執行堆棧(包含github地址):
結束語
本文重點從狀態機設計模式的角度,演示了async/await語法糖的內部實現。
通過一個騷動的機器引擎,演示了開啓異步任務---> 異步任務完成---> 設置狀態機輸出結果的全過程,而這4個狀態的變遷又催生了.NET異步編程的帶來的性能優勢。
最後:本文是一線碼農大佬(博客園12349粉絲博主)《異步async/await底朝天》的狗尾續貂,respect !!!