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的实现

 

讲到这里,您或许已经猜测到八九分了,这个await关键字的出现位置、使用方式以及运行逻辑和yield是如此的像,不错,await和async就是一种基于编译器的功能(C#和VB.NET都提供了这个功能),不仅如此,它在实现原理上也和yield非常像——那就是await/async和yield都是被编译器转化为了状态机。

 

状态机是一种非常常用的编程模式,基本上所有的编译器都是基于状态机实现的,当您看这篇博文的时候,浏览器就是使用状态机将从cnblog.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异步编程,主要代码如下:

 
  
    public partial class MainWindow : Window    {        // Mark the event handler with async so you can use await in it.        private async void StartButton_Click(object sender, RoutedEventArgs e)        {            // Call and await separately.            //Task<int> getLengthTask = AccessTheWebAsync();            //// You can do independent work here.            //int contentLength = await getLengthTask;            int contentLength = await AccessTheWebAsync();            resultsTextBox.Text +=                String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength);        }        // Three things to note in the signature:        //  - The method has an async modifier.         //  - The return type is Task or Task<T>. (See "Return Types" section.)        //    Here, it is Task<int> because the return statement returns an integer.        //  - The method name ends in "Async."        async Task<int> AccessTheWebAsync()        {             // You need to add a reference to System.Net.Http to declare client.            HttpClient client = new HttpClient();            // GetStringAsync returns a Task<string>. That means that when you await the            // task you'll get a string (urlContents).            Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");            // You can do work here that doesn't rely on the string from GetStringAsync.            DoIndependentWork();            // The await operator suspends AccessTheWebAsync.            //  - AccessTheWebAsync can't continue until getStringTask is complete.            //  - Meanwhile, control returns to the caller of AccessTheWebAsync.            //  - Control resumes here when getStringTask is complete.             //  - The await operator then retrieves the string result from getStringTask.            string urlContents = await getStringTask;            // The return statement specifies an integer result.            // Any methods that are awaiting AccessTheWebAsync retrieve the length value.            return urlContents.Length;        }        void DoIndependentWork()        {            resultsTextBox.Text += "Working . . . . . . .\r\n";        }    }
 
 

用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__this12             public MainWindow u003cu003e4__this;13 14             // <client>5__515             public HttpClient u003cclientu003e5__5;16 17             // <getStringTask>5__618             public Task<string> u003cgetStringTasku003e5__6;19 20             // <urlContents>5__721             public string u003curlContentsu003e5__7;22 23             // <>u__$awaiter824             private TaskAwaiter<string> u003cu003eu__u0024awaiter8;25 26             // <>t__stack27             private object u003cu003et__stack;28 29             void MoveNext()30             {31                 int <>t__result = 0;32                 TaskAwaiter<string> u003cu003eu_u0024awaiter8;33                 try34                 {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                         else47                         {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,也就是UTF编码。这样反编译出来的程序就能直接拷贝到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调用的异步函数必然在某个地方——也许是嵌套了很深的一个地方——启动了一个新的工作线程来完成导致我们要使用异步调用的耗时比较长的工作,比如网络内容读取。

 

看上面的代码,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 7910             6e 63 3e 64 5f 5f 34 00 0011         )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_216         )17 18         IL_0000: ldloca.s V_019         IL_0002: ldarg.020         IL_0003: stfld class AsyncFirstExample.MainWindow AsyncFirstExample.MainWindow/'<AccessTheWebAsync>d__4'::'<>4__this'21         IL_0008: ldloca.s V_022         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_025         IL_0016: ldc.i4.m126         IL_0017: stfld int32 AsyncFirstExample.MainWindow/'<AccessTheWebAsync>d__4'::'<>1__state'27         IL_001c: ldloca.s V_028         IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<int32> AsyncFirstExample.MainWindow/'<AccessTheWebAsync>d__4'::'<>t__builder'29         IL_0023: stloc.230         IL_0024: ldloca.s V_231         IL_0026: ldloca.s V_032         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_034         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.137         IL_003a: br.s IL_003c38 39         IL_003c: ldloc.140         IL_003d: ret41     }
 
 

仔细看这段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到最后必然调用了一个启动了新线程的完成实际工作的真正异步的异步函数,那么如何自己定义一个这样的函数呢?其实很简单,使用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. 结论 

 

综上所述,正如文章开头提到的,Async和Await这两个关键字使得用户能以一种简洁直观的方式实现异步编程,用户甚至不需要改变代码的逻辑结构就能将原来的同步函数改造为异步函数。

 

Async和Await这两个关键字由编译器转换为状态机,通过System.Threading.Tasks中的类实现异步编程。

                   

<script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>           

再分享一下我老师大神的人工智能教程吧。零基础!通俗易懂!风趣幽默!还带黄段子!希望你也加入到我们人工智能的队伍中来!https://blog.csdn.net/jiangjunshow

猜你喜欢

转载自blog.csdn.net/qq_43747015/article/details/87707668
今日推荐