await,async 我要把它翻個底朝天,這回你總該明白了吧

一線碼農發表於2020-08-25

一:背景

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# ,或許你以前很多不明白的地方此時都會豁然開朗,不是嗎?

如您有更多問題與我互動,掃描下方進來吧~

圖片名稱

相關文章