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 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. (See "Return Types" section.)
18         //    Here, it is Task 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. 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         // 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             // 5__5
15             public HttpClient u003cclientu003e5__5;
16 
17             // 5__6
18             public Task<string> u003cgetStringTasku003e5__6;
19 
20             // 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.AwaitUnsafeOnCompletedstring>, 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 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/'d__4' V_0,
14             [1] class [mscorlib]System.Threading.Tasks.Task`1 V_1,
15             [2] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 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/'d__4'::'<>4__this'
21         IL_0008: ldloca.s V_0
22         IL_000a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1::Create()
23         IL_000f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 AsyncFirstExample.MainWindow/'d__4'::'<>t__builder'
24         IL_0014: ldloca.s V_0
25         IL_0016: ldc.i4.m1
26         IL_0017: stfld int32 AsyncFirstExample.MainWindow/'d__4'::'<>1__state'
27         IL_001c: ldloca.s V_0
28         IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 AsyncFirstExample.MainWindow/'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::Start'd__4'>(!!0&)
33         IL_002d: ldloca.s V_0
34         IL_002f: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 AsyncFirstExample.MainWindow/'d__4'::'<>t__builder'
35         IL_0034: call instance class [mscorlib]System.Threading.Tasks.Task`1 valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1::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#。