一:背景
1. 講故事
await,async 這玩意的知識點已經被人說的爛的不能再爛了,看似沒什麼好說的,但我發現有不少文章還是從理論上講述了這兩個語法糖的用法,懂得還是懂,不懂的看似懂了過幾天又不懂了,人生如戲全靠記是不行的哈???,其實本質上來說 await, async 只是編譯器層面上的語法糖,在 IL 層面都會被打成原型的,所以在這個層面上認識這兩個語法糖是非常有必要的。
二:從 IL 層面認識
1. 使用 WebClient 下載
為了方便打回原型,我先上一個例子,使用 webclient 非同步下載 http://cnblogs.com
的html,程式碼如下:
class Program
{
static void Main(string[] args)
{
var html = GetResult();
Console.WriteLine("稍等... 正在下載 cnblogs -> html \r\n");
var content = html.Result;
Console.WriteLine(content);
}
static async Task<string> GetResult()
{
var client = new WebClient();
var content = await client.DownloadStringTaskAsync(new Uri("http://cnblogs.com"));
return content;
}
}
上面的程式碼非常簡單,可以看到非同步操作沒有阻塞主執行緒輸出: 稍等... 正在下載 cnblogs -> html \r\n
, 編譯器層面沒什麼好說的 ,接下來看下在 IL 層面發生了什麼?
2. 挖掘 await async 的IL程式碼
還是老規矩, ilSpy 走起,如下圖:
可以看到,這裡有一個 GetResult
方法 ,一個 Main
方法,還有一個不知道在哪裡冒出來的 <GetResult>d__1
類,接下來和大家一個一個聊。
<1> <GetResult>d__1> 類
因為不知道從哪裡冒出來的,特別引人關注,所以看看它的 IL 是咋樣的?
.class nested private auto ansi sealed beforefieldinit '<GetResult>d__1'
extends [System.Runtime]System.Object
implements [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine
{
.method private final hidebysig newslot virtual
instance void MoveNext () cil managed
{
}
.method private final hidebysig newslot virtual
instance void SetStateMachine (
class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine
) cil managed
{
}
}
從上面的 IL 程式碼可以看到,這是自動生成的 <GetResult>d__1
類實現了介面 IAsyncStateMachine
,定義如下:
看到裡面的 MoveNext
是不是很眼熟,平時你在 foreach 集合的時候就會用到這個方法,那時人家叫做列舉類,在這裡算是被改造了一下, 叫狀態機???。
<2> GetResult ()
為了方便演示,我對方法體中的 IL 程式碼做一下簡化:
.method private hidebysig static
class [System.Runtime]System.Threading.Tasks.Task`1<string> GetResult () cil managed
{
IL_0000: newobj instance void ConsoleApp3.Program/'<GetResult>d__1'::.ctor()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: call valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string>::Create()
IL_000c: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string> ConsoleApp3.Program/'<GetResult>d__1'::'<>t__builder'
IL_0011: ldloc.0
IL_0012: ldc.i4.m1
IL_0013: stfld int32 ConsoleApp3.Program/'<GetResult>d__1'::'<>1__state'
IL_0018: ldloc.0
IL_0019: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string> ConsoleApp3.Program/'<GetResult>d__1'::'<>t__builder'
IL_001e: ldloca.s 0
IL_0020: call instance void valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string>::Start<class ConsoleApp3.Program/'<GetResult>d__1'>(!!0&)
IL_0025: ldloc.0
IL_0026: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string> ConsoleApp3.Program/'<GetResult>d__1'::'<>t__builder'
IL_002b: call instance class [System.Runtime]System.Threading.Tasks.Task`1<!0> valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string>::get_Task()
IL_0030: ret
} // end of method Program::GetResult
如果你稍微懂一點的話,在 IL_0000
處的 newobj 你就應該知道這個方法就是做了 new <GetResult>d__1
,然後從 IL_002b
處返回了一個 get_Task()
,這時候你就應該明白,為什麼主執行緒不會被阻塞,因為人家返回的是 Task<string>
,對吧,最後的 http 結果會藏在 Task<string>
中,這樣是不是就很好理解了。
<3> Main
Main方法沒有做任何改變,原來是什麼樣現在還是什麼樣。
三:將 IL 程式碼 回寫為 C#
1. 完整 C# 程式碼
通過前面一部分你應該對 await ,async 在 IL 層面有了一個框架性的認識,這裡我就全部反寫成 C# 程式碼:
class Program
{
static void Main(string[] args)
{
var html = GetResult();
Console.WriteLine("稍等... 正在下載 cnblogs -> html \r\n");
var content = html.Result;
Console.WriteLine(content);
}
static Task<string> GetResult()
{
GetResult stateMachine = new GetResult();
stateMachine.builder = AsyncTaskMethodBuilder<string>.Create();
stateMachine.state = -1;
stateMachine.builder.Start(ref stateMachine);
return stateMachine.builder.Task;
}
}
class GetResult : IAsyncStateMachine
{
public int state;
public AsyncTaskMethodBuilder<string> builder;
private WebClient client;
private string content;
private string s3;
private TaskAwaiter<string> awaiter;
public void MoveNext()
{
var result = string.Empty;
TaskAwaiter<string> localAwaiter;
GetResult stateMachine;
int num = state;
try
{
if (num == 0)
{
localAwaiter = awaiter;
awaiter = default(TaskAwaiter<string>);
num = state = -1;
}
else
{
client = new WebClient();
localAwaiter = client.DownloadStringTaskAsync(new Uri("http://cnblogs.com")).GetAwaiter();
if (!localAwaiter.IsCompleted)
{
num = state = 0;
awaiter = localAwaiter;
stateMachine = this;
builder.AwaitUnsafeOnCompleted(ref localAwaiter, ref stateMachine);
return;
}
}
s3 = localAwaiter.GetResult();
content = s3;
s3 = null;
result = content;
}
catch (Exception exx)
{
state = -2;
client = null;
content = null;
builder.SetException(exx);
}
state = -2;
client = null;
content = null;
builder.SetResult(result);
}
public void SetStateMachine(IAsyncStateMachine stateMachine) { }
}
可以看到,回寫成 C# 程式碼之後跑起來是沒有任何問題的,為了方便理解,我先來畫一張流程圖。
通過上面的 xmind,它基本流程就是: stateMachine.builder.Start(ref stateMachine) -> GetResult.MoveNext -> client.DownloadStringTaskAsync -> localAwaiter.IsCompleted = false -> builder.AwaitUnsafeOnCompleted(ref localAwaiter, ref stateMachine) -> GetResult.MoveNext -> localAwaiter.GetResult() -> builder.SetResult(result)
2. 剖析 AsyncTaskMethodBuilder
其實你仔細觀察會發現,所謂的 await,async 的非同步化運作都是由 AsyncTaskMethodBuilder 承載的,如非同步任務的啟動,對html結果的封送,接觸底層IO,其中 Task<string>
對應著 AsyncTaskMethodBuilder<string>
, Task 對應著 AsyncTaskMethodBuilder, 這也是為什麼編譯器在 async 處一直提示你返回 Task 和 Task<string>
,如果不這樣的話的就找不到對應 AsyncTaskMethodBuilder 了,對吧,如下圖:
然後著重看下 AwaitUnsafeOnCompleted
方法,這個方法非常重要,其註釋如下:
//
// Summary:
// Schedules the state machine to proceed to the next action when the specified
// awaiter completes. This method can be called from partially trusted code.
public void AwaitUnsafeOnCompleted<[NullableAttribute(0)] TAwaiter, [NullableAttribute(0)] TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine;
一旦呼叫了這個方法,就需要等待 底層IO 將任務處理完畢之後二次回撥 GetResult.MoveNext
,也就表示要麼異常要麼完成任務, Awaiter 包裝的 Task 結果封送到 builder.SetResult
。
然後簡單說一下 狀態機 的走法,通過除錯會發現這裡會走 兩次 MoveNext,一次啟動,一次拿結果。
<1> 第一次回撥 MoveNext
第一次 MoveNext 的觸發由 stateMachine.builder.Start(ref stateMachine) 發起,可以用 dnspy 去除錯一下,如下圖:
<2> 第二次回撥 MoveNext
第二次 MoveNext 的觸發由 builder.AwaitUnsafeOnCompleted(ref localAwaiter, ref stateMachine) 開始,可以看到一旦 網路驅動程式 處理完畢後就由執行緒池IO執行緒主動發起到最後觸發程式碼中的 MoveNext,最後就是到 awaiter 中獲取 task 的 result 處結束,如下圖:
四: 總結
語法糖有簡單和複雜之分,複雜的也不要怕,學會將 IL 程式碼翻譯成 C# ,或許你以前很多不明白的地方此時都會豁然開朗,不是嗎?