Visual Studio Async CTP的实现原理浅析 - 如何不使用async和await关键字来实现Async
时间:2011-03-31 来源:王燕丽
Async CTP为我们在单线程实现异步操作开辟了一条大道,尤其对于SL中的WCF来说让我们从繁琐的事件处理中解脱出来,本来我想写一个SL中使用Socket实现的RPC框架(正在我项目中使用)的系列笔记,不过有朋友提到了应该用Async CTP来规避WCF的异步方法带来的繁琐,事实上Async CTP在我项目中还不够灵活(应该是WCF对我的应用来说不够灵活),不过还是先放下那个系列,先看看Async CTP可以做些什么。
本文不会介绍Async CTP的使用方法,只分析在不改动SL CLR的基础上编译器如何根据async和await两个关键字对我们的代码做出正确的改动以达到单线程异步操作。
关于单线程异步可以参考 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。
为了实现async,await编译器将每个被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关键字,阻止编译器对代码的改动,这样的好处是断点调试可以正常进行。