Async和Await異步編程的原理


1. 簡介 

從4.0版本開始.NET引入並行編程庫,用戶能夠通過這個庫快捷的開發並行計算和並行任務處理的程序。在4.5版本中.NET又引入了Async和Await兩個新的關鍵字,在語言層面對並行編程給予進一步的支持,使得用戶能以一種簡潔直觀的方式實現並行編程。因為在很多文檔里針對Async和Await這兩個關鍵字的使用都被稱為異步編程,為了更符合大眾的閱讀習慣,我們使用異步編程這個叫法,意思上和並行編程完全一樣。

關於Async和Await異步編程的功能說明和使用介紹,MSDN上有詳細文檔,鏈接如下:

http://msdn.microsoft.com/en-us/library/vstudio/hh191443.aspx

其它地方也可以搜索到很多相關文章,這里就不再贅述,本文主要介紹的是異步編程是如何現實的,背后的原理是什么。

注意:在您閱讀下面內容之前請確保已經熟悉了異步編程的基本方法。

2. .NET中提供新功能的幾種方法

在繼續之前總結一下.NET中提供新功能的三種方法:基於運行時、基於編譯器和基於類庫。

2.1 基於運行時的實現

顯而易見.NET中大多數功能都是基於運行時實現的。比如的類定義的語法、方法的調用的語法以及所有基本編程語法都有對應的IL代碼,這也正是定義運行時的內容之一。所以能編譯為對應專有IL代碼的功能必然是基於運行時實現的。

2.2 基於編譯器

基於編譯器的實現,最常見的例子就是上下文using和yield。上下文using在VB.NET里干脆就沒有對應的語法,C#編譯器替你做了你在老版本的C#中或VB.NET里要做的工作,就是寫try、finally和Dispose語句。提供基於編譯器的新功能微軟不需要修改運行時。

2.3 基於類庫

這個不需要太多解釋,所有的編程語言都是通過庫為開發者提供強大的開發功能的,庫的豐富程度最終決定一個語言的發展前景。

.NET現在常用的運行時只有2.0和4.0兩個版本,3.0 和3.5都是2.0的運行時;4.5的運行時是4.0,它是在編譯器功能和類庫上對4.0的擴展。

3. Async和Await的實現

前面提到了yield關鍵字,用於簡化遍歷的實現。如果您熟悉yield這個關鍵字的應用,就會發現await關鍵字的出現位置、使用方式以及運行邏輯和yield是如此的相似。事實的確如此,await和async也是一種基於編譯器的功能(C#和VB.NET都提供了這個功能),不僅如此,它在實現原理上也和yield非常像——await/async和yield都被編譯器在編譯時轉化為了狀態機。

狀態機是一種非常常用的編程模式,基本上所有的編譯器都是基於狀態機實現的,當訪問這篇博文的時候瀏覽器就是使用狀態機將從cnblogs.com服務器上獲取的html文本解析為html元素樹,再繪制到屏幕上。

如何發現或者證實這一點呢,那就是用.NET的反編譯器,每當出現新語法,但凡好奇者都喜歡用反編譯器看一下生成的IL代碼究竟是什么樣子。在Reflector被收購收費后(引來吐槽無數),就一直使用JustDecompile(Telerik在Reflector收費后立即推出的免費程序),使用JustDecompile時,需要在該程序的Settings中將Show compiler generated types and members選中。也可以用.NET SDK自帶的ILDASM來反編譯,功能雖然最強大,但是只能反編譯為IL匯編語言,用起來有些不便。

首先,下載MSDN上的示例Async Sample Example from Asynchronous Programming with Async and Await,這是一個簡單的WPF應用,用於演示Async/Await異步編程,主要代碼如下:

 1     public partial class MainWindow : Window
 2     {
 3         // Mark the event handler with async so you can use await in it.
 4         private async void StartButton_Click(object sender, RoutedEventArgs e)
 5         {
 6             // Call and await separately.
 7             //Task<int> getLengthTask = AccessTheWebAsync();
 8             //// You can do independent work here.
 9             //int contentLength = await getLengthTask;
10             int contentLength = await AccessTheWebAsync();
11             resultsTextBox.Text +=
12                 String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength);
13         }
14 
15         // Three things to note in the signature:
16         //  - The method has an async modifier. 
17         //  - The return type is Task or Task<T>. (See "Return Types" section.)
18         //    Here, it is Task<int> because the return statement returns an integer.
19         //  - The method name ends in "Async."
20         async Task<int> AccessTheWebAsync()
21         { 
22             // You need to add a reference to System.Net.Http to declare client.
23             HttpClient client = new HttpClient();
24 
25             // GetStringAsync returns a Task<string>. That means that when you await the
26             // task you'll get a string (urlContents).
27             Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");
28 
29             // You can do work here that doesn't rely on the string from GetStringAsync.
30             DoIndependentWork();
31 
32             // The await operator suspends AccessTheWebAsync.
33             //  - AccessTheWebAsync can't continue until getStringTask is complete.
34             //  - Meanwhile, control returns to the caller of AccessTheWebAsync.
35             //  - Control resumes here when getStringTask is complete. 
36             //  - The await operator then retrieves the string result from getStringTask.
37             string urlContents = await getStringTask;
38 
39             // The return statement specifies an integer result.
40             // Any methods that are awaiting AccessTheWebAsync retrieve the length value.
41             return urlContents.Length;
42         }
43 
44         void DoIndependentWork()
45         {
46             resultsTextBox.Text += "Working . . . . . . .\r\n";
47         }
48     }

 然后,用JustDecompile打開生成的AsyncFirstExample.exe。類視圖如下:

這時可以看到,MainWindow類中多出了兩個名稱以u003c開頭的類,這兩個類就是狀態機類,代碼中有兩個async函數,因此生成了兩個狀態機類。

因為編譯器轉換每個async函數的方式都一樣,所以下面的內容中都以AccessTheWebAsync這個函數為例來說明,該函數對應的狀態機類為u003cAccessTheWebAsyncu003ed__4,反編譯后的C#代碼如下:

 1         [CompilerGenerated]
 2         // <AccessTheWebAsync>d__4
 3         private struct u003cAccessTheWebAsyncu003ed__4 : IAsyncStateMachine
 4         {
 5             // <>1__state
 6             public int u003cu003e1__state;
 7 
 8             // <>t__builder
 9             public AsyncTaskMethodBuilder<int> u003cu003et__builder;
10 
11             // <>4__this
12             public MainWindow u003cu003e4__this;
13 
14             // <client>5__5
15             public HttpClient u003cclientu003e5__5;
16 
17             // <getStringTask>5__6
18             public Task<string> u003cgetStringTasku003e5__6;
19 
20             // <urlContents>5__7
21             public string u003curlContentsu003e5__7;
22 
23             // <>u__$awaiter8
24             private TaskAwaiter<string> u003cu003eu__u0024awaiter8;
25 
26             // <>t__stack
27             private object u003cu003et__stack;
28 
29             void MoveNext()
30             {
31                 int <>t__result = 0;
32                 TaskAwaiter<string> u003cu003eu_u0024awaiter8;
33                 try
34                 {
35                     bool <>t__doFinallyBodies = true;
36                     int u003cu003e1_state = this.u003cu003e1__state;
37                     if (u003cu003e1_state != -3)
38                     {
39                         if (u003cu003e1_state == 0)
40                         {
41                             u003cu003eu_u0024awaiter8 = this.u003cu003eu__u0024awaiter8;
42                             TaskAwaiter<string> taskAwaiter = new TaskAwaiter<string>();
43                             this.u003cu003eu__u0024awaiter8 = taskAwaiter;
44                             this.u003cu003e1__state = -1;
45                         }
46                         else
47                         {
48                             this.u003cclientu003e5__5 = new HttpClient();
49                             this.u003cgetStringTasku003e5__6 = this.u003cclientu003e5__5.GetStringAsync("http://msdn.microsoft.com");
50                             this.u003cu003e4__this.DoIndependentWork();
51                             u003cu003eu_u0024awaiter8 = this.u003cgetStringTasku003e5__6.GetAwaiter();
52                             if (!u003cu003eu_u0024awaiter8.IsCompleted)
53                             {
54                                 this.u003cu003e1__state = 0;
55                                 this.u003cu003eu__u0024awaiter8 = u003cu003eu_u0024awaiter8;
56                                 this.u003cu003et__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, MainWindow.u003cAccessTheWebAsyncu003ed__4>(ref u003cu003eu_u0024awaiter8, this);
57                                 <>t__doFinallyBodies = false;
58                                 return;
59                             }
60                         }
61                         string result = u003cu003eu_u0024awaiter8.GetResult();
62                         u003cu003eu_u0024awaiter8 = new TaskAwaiter<string>();
63                         this.u003curlContentsu003e5__7 = result;
64                         <>t__result = this.u003curlContentsu003e5__7.Length;
65                     }
66                 }
67                 catch (Exception exception)
68                 {
69                     Exception <>t__ex = exception;
70                     this.u003cu003e1__state = -2;
71                     this.u003cu003et__builder.SetException(<>t__ex);
72                     return;
73                 }
74                 this.u003cu003e1__state = -2;
75                 this.u003cu003et__builder.SetResult(<>t__result);
76             }
77 
78             [DebuggerHidden]
79             void SetStateMachine(IAsyncStateMachine param0)
80             {
81                 this.u003cu003et__builder.SetStateMachine(param0);
82             }
83         }

關於這個類的命名,C#編譯器命名編譯器生成的類和類成員的方式是:<生成來源名稱>__后綴或輔助說明信息。尖括號在絕大多數語言中都是運算符,不能用作程序中標識符的命名,但在IL中,標識符都以字符串的形式保存在元數據中,通過映射的數字(一般是元數據內的本地偏移地址)來表示標識符,因此對標識符的命名基本沒有限制。C#編譯器利用這一點,在編譯器生成的IL代碼中通過使用<和>來明確區分用戶寫的代碼和編譯器自動生成的代碼。

因為<和>不能用在C#的標識符命名中,反編譯程序JustDecompile對此做出了處理,將<轉換為u003c,>轉換為u003e,也就是Unicode編碼。這樣反編譯出來的程序就能直接拷貝到C#編輯器中使用,但是這個版本的JustDecompile存在一個bug,就是局部變量中的<和>並沒有被正確的轉換為u003c和u003e,所以生成的代碼還是不能直接拷貝就用的,當然這並不影響解讀這段代碼。

類u003cAccessTheWebAsyncu003ed__4實現了接口IAsyncStateMachine,從名字可以看出,這個接口就是為異步編程定義的。這個接口只有兩個方法MoveNext和SetStateMachine,一個典型的狀態機定義:執行下一步和設置狀態。用一個簡單的例子快速梳理一下狀態機的工作過程,以幫助理解異步編程的機制:

一個有1和2兩個有效狀態的狀態機,如果狀態值為1,調用MoveNext時狀態機會執行操作A同時將狀態值改為2;如果狀態值為2,調用MoveNext時狀態機會執行操作B同時將狀態值改為3;如果狀態值為3,調用MoveNext時狀態機不執行任何操作或拋出異常。

在上面的這個簡單狀態機中,調用者不需要知道狀態機下一步要干什么,它只被告知在某個時候需要調用MoveNext,具體干什么由狀態機的內部實現決定,異步編程就是利用的這種模式,通過編譯器對代碼進行重組,將一個await調用前和調用后執行的代碼分配到狀態機的兩個狀態中去執行。如果一個async函數中有兩個await調用,那么生成的狀態機就會有3個狀態,以此類推。如果有循環,根據循環的位置不同,狀態機狀態轉換更復雜一些。

回過頭來看異步編程中的異步。在學習使用async/await的時候,很多文檔包括msdn都刻意提到async/await關鍵字不會創建新的線程,用async關鍵字寫的函數中的代碼都在調用線程中執行。這里是最容易混淆的地方,嚴格意義上這個說法不准確,異步編程必然是多線程的。msdn文檔里提到的不會創建新線程應該是指async函數本身不會直接在新線程中運行。本質上是await調用的異步函數執行完成后回調狀態機的MoveNext來執行余下未執行完成的代碼,await調用的異步函數必然在某個地方——也許是嵌套了很深的一個地方——啟動了一個新的工作線程來完成導致我們要使用異步調用的耗時比較長的工作,比如網絡內容讀取。

再看u003cAccessTheWebAsyncu003ed__4類的代碼,u003cu003e1__state這個成員變量很明顯就是狀態值了,在48行到50行,當狀態只不等於-3也不等於0的時候,運行的正好是原始C#代碼中await語句前面的代碼,第52行if (!u003cu003eu_u0024awaiter2.IsCompleted)這里很關鍵,這里正好是異步執行最明顯的體現,那就是當主線程里DoIndependentWork()運行結束的時候,另一個線程里獲取http://msdn.microsoft.com頁面內容的工作的也可能已經完成了。如果獲取頁面的工作完成了,就可以直接運行下一狀態要運行的代碼(62行到64行,原始C#代碼中await語句后面的代),而不需要進入等待;如果獲取頁面的工作還沒有完成,執行第54到58行代碼,將當前狀態機與TaskAwaiter綁定,同時將狀態機的狀態值改為0,當異步函數在另一個線程中執行完成時,TaskAwaiter回調狀態機的MoveNext函數,這時狀態機的狀態為0,運行62到64行代碼,完成AcessTheWebAsync函數的工作。

可見AcessTheWebAsync函數中原有的代碼都被編譯器重組到狀態機中了,那么AcessTheWebAsync函數現在干什么?可以猜想到的就是創建狀態機實例,設置初始狀態(不等於-3也不等於0)和啟動狀態機。究竟是不是這樣,來看AcessTheWebAsync反編譯出來的C#代碼:

 1         private async Task<int> AccessTheWebAsync()
 2         {
 3             HttpClient httpClient = new HttpClient();
 4             Task<string> stringAsync = httpClient.GetStringAsync("http://msdn.microsoft.com");
 5             this.DoIndependentWork();
 6             string str = await stringAsync;
 7             string str1 = str;
 8             int length = str1.Length;
 9             return length;
10         }

似乎函數AcessTheWebAsync的代碼和原始的代碼一樣,編譯器並沒有做修改,真的是這樣嗎?答案是否定的,原因是JustDecompile這個反編譯器太強大了,它竟然將C#編譯器轉換的代碼重新還原成async/await語法的代碼了。所以這里我們只能看IL代碼了,切換到IL代碼,可以看到AcessTheWebAsync編譯后的最終的代碼如下:

 1     .method private hidebysig instance class [mscorlib]System.Threading.Tasks.Task`1<int32> AccessTheWebAsync () cil managed 
 2     {
 3         .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
 4             01 00 00 00
 5         )
 6         .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
 7             01 00 34 41 73 79 6e 63 46 69 72 73 74 45 78 61
 8             6d 70 6c 65 2e 4d 61 69 6e 57 69 6e 64 6f 77 2b
 9             3c 41 63 63 65 73 73 54 68 65 57 65 62 41 73 79
10             6e 63 3e 64 5f 5f 34 00 00
11         )
12         .locals init (
13             [0] valuetype AsyncFirstExample.MainWindow/'<AccessTheWebAsync>d__4' V_0,
14             [1] class [mscorlib]System.Threading.Tasks.Task`1<int32> V_1,
15             [2] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> V_2
16         )
17 
18         IL_0000: ldloca.s V_0
19         IL_0002: ldarg.0
20         IL_0003: stfld class AsyncFirstExample.MainWindow AsyncFirstExample.MainWindow/'<AccessTheWebAsync>d__4'::'<>4__this'
21         IL_0008: ldloca.s V_0
22         IL_000a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Create()
23         IL_000f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> AsyncFirstExample.MainWindow/'<AccessTheWebAsync>d__4'::'<>t__builder'
24         IL_0014: ldloca.s V_0
25         IL_0016: ldc.i4.m1
26         IL_0017: stfld int32 AsyncFirstExample.MainWindow/'<AccessTheWebAsync>d__4'::'<>1__state'
27         IL_001c: ldloca.s V_0
28         IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> AsyncFirstExample.MainWindow/'<AccessTheWebAsync>d__4'::'<>t__builder'
29         IL_0023: stloc.2
30         IL_0024: ldloca.s V_2
31         IL_0026: ldloca.s V_0
32         IL_0028: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::Start<valuetype AsyncFirstExample.MainWindow/'<AccessTheWebAsync>d__4'>(!!0&)
33         IL_002d: ldloca.s V_0
34         IL_002f: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> AsyncFirstExample.MainWindow/'<AccessTheWebAsync>d__4'::'<>t__builder'
35         IL_0034: call instance class [mscorlib]System.Threading.Tasks.Task`1<int32> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32>::get_Task()
36         IL_0039: stloc.1
37         IL_003a: br.s IL_003c
38 
39         IL_003c: ldloc.1
40         IL_003d: ret
41     }

仔細看這段IL匯編代碼,與原始的C#版的AcessTheWebAsync函數相比幾乎沒有任何相似之處,只有函數的聲明相同,這就是編譯器轉換的結果。人工將這段IL匯編代碼反編譯成C#:

 1         [System.Diagnostics.DebuggerStepThrough()]
 2         [System.Runtime.CompilerServices.AsyncStateMachine(typeof(u003cAccessTheWebAsyncu003ed__4))]
 3         private Task<int> AccessTheWebAsync()
 4         {
 5             u003cAccessTheWebAsyncu003ed__4 V_0;
 6             Task<int> V_1;
 7             System.Runtime.CompilerServices.AsyncTaskMethodBuilder<int> V_2;
 8 
 9             V_0.u003cu003e4__this = this;
10             V_0.u003cu003et__builder = System.Runtime.CompilerServices.AsyncTaskMethodBuilder<int>.Create();
11             V_0.u003cu003e1__state = -1;
12             V_2 = V_0.u003cu003et__builder;
13             V_2.Start(ref V_0);
14             V_1 = V_2.Task;
15             return V_1;
16         }

到這里已經非常清楚了:AcessTheWebAsync函數首先創建狀態機的實例,因為狀態機類是Struct類型,不需要new;然后,設置相關屬性,狀態機的初始狀態值被設置為-1,符合之前期望的范圍;最后,啟動狀態機,Start方法內部會調用一次MoveNext,運行結束后返回Task。

多個async函數之間的調用,就是多個狀態機的組合運行。

4. 創建一個真正異步的異步函數

前面提到await語句await到最后必然調用了一個啟動了新線程的完成實際工作的真正異步的異步函數,那么如何自己定義一個這樣的函數呢?其實很簡單,使用System.Threading.Tasks.Task類就可以創建這樣一個函數,示例代碼如下:

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            resultsTextBox.Text += String.Format("\r\nMyAsync({0}).\r\n",
                Thread.CurrentThread.ManagedThreadId); 
            while (true)
                resultsTextBox.Text += String.Format("\r\nMyAsync({0}): {1}.\r\n", 
                    Thread.CurrentThread.ManagedThreadId, await MyAsync());
        }

        public Task<string> MyAsync()
        {
            var t = new Task<string>((str) =>
                {
                    var dt = DateTime.Now;
                    Thread.Sleep(4000);

                    return String.Format("({0}){1} - {2}", 
                        Thread.CurrentThread.ManagedThreadId, dt, DateTime.Now);
                }, null);

            t.Start();
            
            return t;
        }

運行結果如下:

這個程序是在上述msdn提供的示例的基礎上,向界面中加了一個ID為Button的按鈕,它的事件處理函數為Button_Click,MyAsync就是我們要創建的函數。

在這個真正異步的函數里卻看不到Aysnc和Await的影子。由此可見,Aysnc和Await是用來組織異步函數的調用的,實現異步代碼和同步代碼間的無縫交互。

5. 結論 

在.NET 4.5中引入的Async和Await兩個新的關鍵字后,用戶能以一種簡潔直觀的方式實現異步編程。甚至都不需要改變代碼的邏輯結構,就能將原來的同步函數改造為異步函數。

在內部實現上,Async和Await這兩個關鍵字由編譯器轉換為狀態機,通過System.Threading.Tasks中的並行類實現代碼的異步執行。

最后,一定要確保您的項目的.NET Framework的版本為4.5以上。

 

2013-07-22 

關於Async/Await的官方博客:

http://blogs.msdn.com/b/pfxteam/archive/2012/04/12/10293335.aspx

Await, SynchronizationContext, and Console Apps

 

如回復中所言,ILSpy確實更好一些,它有一個選項:顯示原始代碼還是編譯器轉換后的代碼。這樣,如果用ILSpy,我們就能直接看到AccessTheWebAsync函數被編譯器轉換后的C#代碼了,而不需要像文中那樣需要人工將IL轉換為C#。

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM