C# 5.0新特性:Async和Await使异步编程更简单

2013-09-30 09:56:37 | 新闻来源:叶凡网络 | 点击量:955

一、引言

      在之前的C#基础知识系列文章中只介绍了从C#1.0到C#4.0中主要的特性,然而.NET 4.5 的推出,对于C#又有了新特性的增加——就是C#5.0中async和await两个关键字,这两个关键字简化了异步编程,之所以简化了,还是因为编译器给我们做了更多的工作,下面就具体看看编译器到底在背后帮我们做了哪些复杂的工作的。

二、同步代码存在的问题

      对于同步的代码,大家肯定都不陌生,因为我们平常写的代码大部分都是同步的,然而同步代码却存在一个很严重的问题,例如我们向一个Web服务器发出一个请求时,如果我们发出请求的代码是同步实现的话,这时候我们的应用程序就会处于等待状态,直到收回一个响应信息为止,然而在这个等待的状态,对于用户不能操作任何的UI界面以及也没有任何的消息,如果我们试图去操作界面时,此时我们就会看到"应用程序为响应"的信息(在应用程序的窗口旁),相信大家在平常使用桌面软件或者访问web的时候,肯定都遇到过这样类似的情况的,对于这个,大家肯定会觉得看上去非常不舒服。引起这个原因正是因为代码的实现是同步实现的,所以在没有得到一个响应消息之前,界面就成了一个"卡死"状态了,所以这对于用户来说肯定是不可接受的,因为如果我要从服务器上下载一个很大的文件时,此时我们甚至不能对窗体进行关闭的操作的。为了具体说明同步代码存在的问题(造成界面开始),下面通过一个程序让大家更形象地看下问题所在:

  1. // 单击事件  
  2.         private void btnClick_Click(object sender, EventArgs e)  
  3.         {  
  4.             this.btnClick.Enabled = false;  
  5.  
  6.             long length = AccessWeb();  
  7.             this.btnClick.Enabled = true;  
  8.             // 这里可以做一些不依赖回复的操作  
  9.             OtherWork();  
  10.  
  11.             this.richTextBox1.Text += String.Format("\n 回复的字节长度为:  {0}.\r\n", length);  
  12.             txbMainThreadID.Text = Thread.CurrentThread.ManagedThreadId.ToString();  
  13.         }  
  14.  
  15.         private  long AccessWeb()  
  16.         {  
  17.             MemoryStream content = new MemoryStream();  
  18.  
  19.             // 对MSDN发起一个Web请求  
  20.             HttpWebRequest webRequest = WebRequest.Create("http://msdn.microsoft.com/zh-cn/"as HttpWebRequest;  
  21.             if (webRequest != null)  
  22.             {  
  23.                 // 返回回复结果  
  24.                 using (WebResponse response = webRequest.GetResponse())  
  25.                 {  
  26.                     using (Stream responseStream = response.GetResponseStream())  
  27.                     {  
  28.                         responseStream.CopyTo(content);  
  29.                     }  
  30.                 }  
  31.             }  
  32.  
  33.             txbAsynMethodID.Text = Thread.CurrentThread.ManagedThreadId.ToString();  
  34.             return content.Length;  
  35.         } 

运行程序后,当我们点击窗体的 "点击我"按钮之后,在得到服务器响应之前,我们不能对窗体进行任何的操作,包括移动窗体,关闭窗体等,具体运行结果如下:

三、传统的异步编程来改善程序的响应

     上面部分我们已经看到同步方法所带来的实际问题了,为了解决类似的问题,.NET Framework很早就提供了对异步编程的支持,下面就用.NET 1.0中提出的异步编程模型(APM)来解决上面的问题,具体代码如下(注释的部分通过获得GUI线程的同步上文对象,然后同步调用同步上下文对象的post方法把要调用的方法交给GUI线程去处理,因为控件本来就是由GUI线程创建的,然后由它自己执行访问控件的操作就不存在跨线程的问题了,程序中使用的是调用RichTextBox控件的Invoke方式来异步回调访问控件的方法,其实背后的原来和注释部分是一样的,调用RichTextBox控件的Invoke方法可以获得创建RichTextBox控件的线程信息(也就是前一种方式的同步上下文),然后让Invoke回调的方法在该线程上运行):

  1. private void btnClick_Click(object sender, EventArgs e)  
  2.         {  
  3.             
          this.richTextBox1.Clear();  
           btnClick.Enabled = false; 
     
  4.             AsyncMethodCaller caller = new AsyncMethodCaller(TestMethod);
  5.  
  6.             IAsyncResult result = caller.BeginInvoke(GetResult, null);  
  7.  
  8.             //// 捕捉调用线程的同步上下文派生对象  
  9.             //sc= SynchronizationContext.Current;  
  10.         }  
  11.      
  12.         # region 使用APM实现异步编程  
  13.         // 同步方法  
  14.         private string TestMethod()  
  15.         {         
  16.             // 模拟做一些耗时的操作  
  17.             // 实际项目中可能是读取一个大文件或者从远程服务器中获取数据等。  
  18.             for (int i = 0; i < 10; i++)  
  19.             {  
  20.                 Thread.Sleep(200);  
  21.             }  
  22.  
  23.             return "点击我按钮事件完成";  
  24.         }  
  25.          
  26.         // 回调方法  
  27.         private void GetResult(IAsyncResult result)  
  28.         {  
  29.             AsyncMethodCaller caller = (AsyncMethodCaller)((AsyncResult)result).AsyncDelegate;  
  30.             // 调用EndInvoke去等待异步调用完成并且获得返回值  
  31.             // 如果异步调用尚未完成,则 EndInvoke 会一直阻止调用线程,直到异步调用完成  
  32.             string resultvalue = caller.EndInvoke(result);  
  33.             //sc.Post(ShowState,resultvalue);  
  34.             richTextBox1.Invoke(showStateCallback, resultvalue);  
  35.         }  
  36.  
  37.         // 显示结果到richTextBox  
  38.         private void ShowState(object result)  
  39.         {  
  40.             richTextBox1.Text = result.ToString();  
  41.             btnClick.Enabled = true;  
  42.         }  
  43.  
  44.         // 显示结果到richTextBox  
  45.         //private void ShowState(string result)  
  46.         //{  
  47.         //    richTextBox1.Text = result;  
  48.  
  49.         //    btnClick.Enabled = true;  
  50.  
  51.         //}  
  52.         #endregion 

运行的结果为:

四、C# 5.0 提供的async和await使异步编程更简单

     上面部分演示了使用传统的异步编程模型(APM)来解决同步代码所存在的问题,然而在.NET 2.0,.NET 4.0和.NET 4.5中,微软都有推出新的方式来解决同步代码的问题,他们分别为基于事件的异步模式,基于任务的异步模式和提供async和await关键字来对异步编程支持。关于前两种异步编程模式,在我前面的文章中都有介绍,大家可以查看相关文章进行详细地了解,本部分就C# 5.0中的async和await这两个关键字如何实现异步编程的问题来给大家介绍下。下面通过代码来了解下如何使用async和await关键字来实现异步编程,并且大家也可以参看前面的博客来对比理解使用async和await是异步编程更简单。

  1. private async void btnClick_Click(object sender, EventArgs e)  
  2.         {  
  3.             long length = await AccessWebAsync();  
  4.              
  5.             // 这里可以做一些不依赖回复的操作  
  6.             OtherWork();  
  7.  
  8.             this.richTextBox1.Text += String.Format("\n 回复的字节长度为:  {0}.\r\n", length);  
  9.             txbMainThreadID.Text = Thread.CurrentThread.ManagedThreadId.ToString();  
  10.         }  
  11.  
  12.         // 使用C# 5.0中提供的async 和await关键字来定义异步方法  
  13.         // 从代码中可以看出C#5.0 中定义异步方法就像定义同步方法一样简单。  
  14.         // 使用async 和await定义异步方法不会创建新线程,  
  15.         // 它运行在现有线程上执行多个任务.  
  16.         // 此时不知道大家有没有一个疑问的?在现有线程上(即UI线程上)运行一个耗时的操作时,  
  17.         // 为什么不会堵塞UI线程的呢?  
  18.         // 这个问题的答案就是 当编译器看到await关键字时,线程会  
  19.         private async Task<long> AccessWebAsync()  
  20.         {  
  21.             MemoryStream content = new MemoryStream();  
  22.  
  23.             // 对MSDN发起一个Web请求  
  24.             HttpWebRequest webRequest = WebRequest.Create("http://msdn.microsoft.com/zh-cn/"as HttpWebRequest;  
  25.             if (webRequest != null)  
  26.             {  
  27.                 // 返回回复结果  
  28.                 using (WebResponse response = await webRequest.GetResponseAsync())  
  29.                 {  
  30.                     using (Stream responseStream = response.GetResponseStream())  
  31.                     {  
  32.                         await responseStream.CopyToAsync(content);  
  33.                     }  
  34.                 }  
  35.             }  
  36.  
  37.             txbAsynMethodID.Text = Thread.CurrentThread.ManagedThreadId.ToString() ;  
  38.             return content.Length;  
  39.         }  
  40.  
  41.         private void OtherWork()  
  42.         {  
  43.             this.richTextBox1.Text += "\r\n等待服务器回复中.................\n";  
  44.         } 

运行结果如下:

对于按钮点击事件的代码来说,编译器生成的背后代码却是下面这样的,完全和我们源码中的两个样:

  1. // 编译器为按钮Click事件生成的代码  
  2. private void btnClick_Click(object sender, EventArgs e)  
  3. {  
  4.     d__0 d__;  
  5.     d__.<>4__this = this;  
  6.     d__.sender = sender;  
  7.     d__.e = e;  
  8.     d__.<>t__builder = AsyncVoidMethodBuilder.Create();  
  9.     d__.<>1__state = -1;  
  10.     d__.<>t__builder.Start(ref d__);  

      看到上面的代码,作为程序员的我想说——编译器你怎么可以这样呢?怎么可以任意篡改我的代码呢?这样不是侵犯我的版权了吗?你要改最起码应该告诉我一声吧,如果我的源码看到它在编译器中的实现是上面那样的,我相信我的源码会说——难道我中了世间上最恶毒的面目全非脚吗? 好吧,为了让大家更好地理清编译器背后到底做了什么事情,下面就顺着上面的代码摸瓜,我也来展示耍一套还我漂漂拳来帮助大家找到编译器代码和源码的对应关系。我的分析思路为:

      1、提出问题——我的click事件的源码到哪里去了呢?从编译器代码我们可以看到,前面的7句代码都是对某个类进行赋值的操作,最真正起作用的就是最后Start方法的调用。这里又产生了几个疑问——d__0是什么类型? 该类型中的<>t__builder字段类型的Start方法到底是做什么用的? 有了这两个疑问,我们就点击d__0(反射工具可以让我们直接点击查看)来看看它是什么类型

  1. // d__0类型的定义,从下面代码可以看出它是一个结构体  
  2. // 该类型是编译器生成的一个嵌入类型  
  3. // 看到该类型的实现有没有让你联想到什么?  
  4. private struct d__0 : IAsyncStateMachine  
  5. {  
  6.     // Fields  
  7.     public int <>1__state;  
  8.     public Form1 <>4__this;  
  9.     public AsyncVoidMethodBuilder <>t__builder;  
  10.     private object <>t__stack;  
  11.     private TaskAwaiter<long> <>u__$awaiter2;  
  12.     public long 5__1;  
  13.     public EventArgs e;  
  14.     public object sender;  
  15.  
  16.     // Methods  
  17.     private void MoveNext()  
  18.     {  
  19.         try 
  20.         {  
  21.             TaskAwaiter<long> CS$0$0001;  
  22.             bool <>t__doFinallyBodies = true;  
  23.             switch (this.<>1__state)  
  24.             {  
  25.                 case -3:  
  26.                     goto Label_010E;  
  27.  
  28.                 case 0:  
  29.                     break;  
  30.  
  31.                 default:  
  32. // 获取用于等待Task(任务)的等待者。你要知道某个任务是否完成,我们就需要一个等待者对象对该任务进行一个监控,所以微软就定义了一个等待者对象的  
  33. // 从这里可以看出,其实async和await关键字背后的实现原理是基于任务的异步编程模式(TAP)  
  34.                     // 这里代码是在线程池线程上运行的  
  35.                     CS$0$0001 = this.<>4__this.AccessWebAsync().GetAwaiter();  
  36. // 如果任务完成就调转到Label_007A部分的代码  
  37.                     if (CS$0$0001.IsCompleted)  
  38.                     {  
  39.                         goto Label_007A;  
  40.                     }  
  41.    
  42.                     // 设置状态为0为了退出回调方法。  
  43.                     this.<>1__state = 0;  
  44.                     this.<>u__$awaiter2 = CS$0$0001;  
  45. // 这个代码是做什么用的呢?让我们带着问题看下面的分析  
  46. this.<>t__builder.AwaitUnsafeOnCompletedlong>, Form1.d__0>(ref CS$0$0001, ref this);  
  47.                     <>t__doFinallyBodies = false;  
  48. // 返回到调用线程,即GUI线程,这也是该方法不会堵塞GUI线程的原因,不管任务是否完成都返回到GUI线程  
  49.                     return;  
  50.             }  
  51.             // 当任务完成时,不会执行下面的代码,会直接执行Label_007A中代码  
  52.             CS$0$0001 = this.<>u__$awaiter2;  
  53.             this.<>u__$awaiter2 = new TaskAwaiter<long>();  
  54.             // 为了使再次回调MoveNext代码  
  55.             this.<>1__state = -1;  
  56.         Label_007A:  
  57.             // 下面代码是在GUI线程上执行的  
  58.             CS$0$0001 = new TaskAwaiter<long>();  
  59.             long CS$0$0003 = CS$0$0001.GetResult();  
  60.             this.5__1 = CS$0$0003;  
  61. // 我们源码中的代码这里的  
  62.             this.<>4__this.OtherWork();  
  63.             this.<>4__this.richTextBox1.Text = this.<>4__this.richTextBox1.Text + string.Format("\n 回复的字节长度为:  {0}.\r\n"this.5__1);  
  64.             this.<>4__this.txbMainThreadID.Text = Thread.CurrentThread.ManagedThreadId.ToString();  
  65.         }  
  66.         catch (Exception <>t__ex)  
  67.         {  
  68.             this.<>1__state = -2;  
  69.             this.<>t__builder.SetException(<>t__ex);  
  70.             return;  
  71.         }  
  72.     Label_010E:  
  73.         this.<>1__state = -2;  
  74.         this.<>t__builder.SetResult();  
  75.     }  
  76.  
  77.     [DebuggerHidden]  
  78.     private void SetStateMachine(IAsyncStateMachine param0)  
  79.     {  
  80.         this.<>t__builder.SetStateMachine(param0);  
  81.     }  
上一篇:在Java Vuser中开发SMTP发送mail脚本 下一篇:台风“菲特”袭击浙江青田石门中学 71名师生被困