现在的位置: 首页 > 综合 > 正文

Visual Studio Async CTP的实现原理浅析 – 如何不使用async和await关键字来实现Async

2012年07月23日 ⁄ 综合 ⁄ 共 3396字 ⁄ 字号 评论关闭

Async CTP为我们在单线程实现异步操作开辟了一条大道,尤其对于SL中的WCF来说让我们从繁琐的事件处理中解脱出来,本来我想写一个SL中使用Socket实现的RPC框架(正在我项目中使用)的系列笔记,不过有朋友提到了应该用Async CTP来规避WCF的异步方法带来的繁琐,事实上Async CTP在我项目中还不够灵活(应该是WCF对我的应用来说不够灵活),不过还是先放下那个系列,先看看Async CTP可以做些什么。

本文不会介绍Async CTP的使用方法,只分析在不改动SL CLR的基础上编译器如何根据asyncawait两个关键字对我们的代码做出正确的改动以达到单线程异步操作。

关于单线程异步可以参考 Programming user interfaces using F# workflows

要理解Async CTP强力推荐此文 Asynchronous Programming in C# using Iterators

先来看看AsyncCtpLibrary_Silverlight.dll中的几个重要的类,在System.Runtime.CompilerServices命名空间中找出这几个类AsyncMehodBuilder, AsyncMethodBuilder<TResult>, VoidAsyncMethodBuilder, TaskAWaiter, TaskAwaiter<TResult>,以及System.Threading.Tasks下面的Task, Task<TResult>看到名字基本可以知道是干啥的了,简单介绍一下:

AsyncMethodBuilder: 假如async被施加在一个Task对象之前,编译器使用这个类。

AsyncMethodBuilder<TResult>: 假如async被施加在Task<TResult>之前, 编译器使用这个类。

VoidAsyncMethodBuilder: async被施加在一个方法声明之前,编译器使用这个类。(因此只有返回值是void的方法能加async关键字)

TaskAwaiter: 对应在Task对象之前的await关键字。

TaskAwaiter<TResult>:对应在Task<TResult>对象之前的await关键字。

Task、Task<TResult>:对应两个await。

为了实现asyncawait编译器将每个被async关键字标记的方法编译为一个方法所在类的一个内嵌类,所有在方法体内出现的变量会被声明为这个类的field,如果是一个实例方法,那么this所代表的对象也被声明为一个field。这个类有两个核心成员:一个int来保存代码执行到那一步,暂且叫它step,一个方法来执行真正的动作,暂且叫做NextStep,整个逻辑看起来应该是这样的:

   1:  public void NextStep()
   2:  {
   3:      switch (step)
   4:      {
   5:          case 1:
   6:              ...
   7:              step++;
   8:              break;
   9:          case 2:
  10:              ...
  11:              step++;
  12:              break;
  13:          case 3:
  14:              ....
  15:              step++;
  16:              break;
  17:          .
  18:          .
  19:          .
  20:      }
  21:  }

是不是觉得和yield return很像?

而在async标记的方法大体是如此,假设方法被编译为一个命为AsyncMethodClass的类:

   1:  AsyncMethodClass a = new AsyncMethodClass();
   2:  a.xxx = xxx;
   3:  a.yyy = yyy;
   4:  .
   5:  .
   6:  .
   7:  a.NextStep();

那么编译根据什么来决定一个方法分成几个块呢?实际上,编译器根据一个async方法中出现的await关键字来进行分布,假如有一个await,那么应该有2个Step,假如有2个await,那么应该有3个Step。每一个Step,应该是以上一个Step中的await的EndWait开始(第一个Step除外),并以下一await的BeginWait结束(最后一个Step除外)。看到这里就理解了为什么async只能被标记在无返回值的函数上,因为NextStep函数必须要是一个无返回值的Action类型,传递给await的BeginWait方法,因此在上述代码中,在最后的第7行没有办法使用理论上的:

   1:  return a.NextStep();

弄明白了编译器干的活之后,最后来看一个实例,来自于Async CTP Samples的片段:

   1:          public async void AsyncIntroSerial()
   2:          {
   3:              var client = new WebClient();
   4:   
   5:              WriteLinePageTitle(await client.DownloadStringTaskAsync(new Uri("http://www.weather.gov")));
   6:              WriteLinePageTitle(await client.DownloadStringTaskAsync(new Uri("http://www.weather.gov/climate/")));
   7:              WriteLinePageTitle(await client.DownloadStringTaskAsync(new Uri("http://www.weather.gov/rss/")));
   8:          }

经过上面的理论,不使用async和await关键字,改写如下:

   1:          TaskAwaiter<string> wait1;
   2:          TaskAwaiter<string> wait2;
   3:          TaskAwaiter<string> wait3;
   4:          int step = 0;
   5:          WebClient wc = new WebClient();
   6:          private void AsyncMethodWithoutKeywords()
   7:          {
   8:              switch (step)
   9:              {
  10:                  case 0:
  11:                      Task<string> t1 = wc.DownloadStringTaskAsync(new Uri("http://www.weather.gov"));
  12:                      step++;
  13:                      wait1 = t1.GetAwaiter<string>();
  14:                      wait1.BeginAwait(new Action(AsyncMethodWithoutKeywords));
  15:                      break;
  16:                  case 1:
  17:                      WriteLinePageTitle(wait1.EndAwait());
  18:                      Task<string> t2 = wc.DownloadStringTaskAsync(new Uri("http://www.weather.gov/climate/"));
  19:                      step++;
  20:                      wait2 = t2.GetAwaiter<string>();
  21:                      wait2.BeginAwait(new Action(AsyncMethodWithoutKeywords));
  22:                      break;
  23:                  case 2:
  24:                      WriteLinePageTitle(wait2.EndAwait());
  25:                      Task<string> t3 = wc.DownloadStringTaskAsync(new Uri("http://www.weather.gov/rss/"));
  26:                      step++;
  27:                      wait3 = t3.GetAwaiter<string>();
  28:                      wait3.BeginAwait(new Action(AsyncMethodWithoutKeywords));
  29:                      break;
  30:                  case 4:
  31:                      WriteLinePageTitle(wait3.EndAwait());
  32:                      break;
  33:              }
  34:          }

可以发现,执行结果和使用async和await一样。

Async CTP的核心其实是Task的设计,CTP库为WebClient和WCF实现了Async到Task的工作,对于项目中出现自定义的一些方法则需要自己去定义Task,不过通过自己的方法定义Task,并且使用本文所描述的方法进行包装(尤其是在原先使用Emit或者使用dynamic关键字来做AOP的场合),那么可以抛开aysnc和await关键字,阻止编译器对代码的改动,这样的好处是断点调试可以正常进行。

抱歉!评论已关闭.