技术开发 频道

.NET异步编程:使用CPS及yield实现异步

  yield 与 异步

  如果你熟悉C# 2.0加入的迭代器特性,你就会发现yield就是我们可以用来打标识的东西。看下面的代码:

  1: public IEnumerator Demo()

  
2: {

  
3: //code 1

  
4: yield return 1;

  
5: //code 2

  
6: yield return 2;

  
7: //code 3

  
8: yield return 3;

  
9: }

  经过编译会生成类似下面的代码(伪代码,相差很远,只是意义相近,想要了解详情的同学可以自行打开Reflector观看):

  1: public IEnumerator Demo()

  
2: {

  
3: return new GeneratedEnumerator();

  
4: }

  
5:

  
6: public class GeneratedEnumerator

  
7: {

  
8: private int state = 0;

  
9:

  
10: private int currentValue = 0;

  
11:

  
12: public bool MoveNext()

  
13: {

  
14: switch(state)

  
15: {

  
16: case 0:

  
17: //code 1

  
18: currentValue = 1;

  
19: state = 1;

  
20: return true;

  
21: case 1:

  
22: //code 2

  
23: currentValue = 2;

  
24: state = 2;

  
25: return true;

  
26: case 2:

  
27: //code 3

  
28: currentValue = 3;

  
29: state = 3;

  
30: return true;

  
31: default:return false;

  
32: }

  
33: }

  
34:

  
35: public int Current{get{return currentValue;}}

  
36: }

  对,C#编译器将其翻译成了一个状态机。yield return就好像做了很多标记,MoveNext每调用一次,它就执行下个yield return之前的代码,然后立即返回。

  好,现在打标记的功能有了,我们如何在异步请求执行完毕后恢复调用呢?通过上面的代码,你可能已经想到了,我们这里恢复调用只需要再次调用一下MoveNext就行了,那个状态机会帮我们处理一切。

  那我们改造我们的异步代码:

  1: public IEnumerator Download()

  
2: {

  
3: var request = HttpWebRequest.Create("http://www.google.com");

  
4: var asyncResult1 = request.BeginGetResponse(...);

  
5: yield return 1;

  
6: var response = request.EndGetResponse(asyncResult1);

  
7: using(stream = response.GetResponseStream())

  
8: {

  
9: var asyncResult2 = stream.BeginRead(buffer, 0, 1024, ...);

  
10: yield return 1;

  
11: var actualRead = stream.EndRead(asyncResult2);

  
12: }

  
13: }

  标记打好了,考虑如何在异步调用完执行一下MoveNext吧。

  呵呵,你还记得异步调用的那个AsyncCallback回调么?也就是异步请求执行完会调用的那个。如果我们向发起异步请求的BeginXXX方法传入一个AsyncCallback,而这个回调里会调用MoveNext怎么样?

  1: public IEnumerator Download(Context context)

  
2: {

  
3: var request = HttpWebRequest.Create("http://www.google.com");

  
4: var asyncResult1 = request.BeginGetResponse(context.Continue(),null);

  
5: yield return 1;

  
6: var response = request.EndGetResponse(asyncResult1);

  
7: using(stream = response.GetResponseStream())

  
8: {

  
9: var asyncResult2 = stream.BeginRead(buffer, 0, 1024, context.Continue(),null);

  
10: yield return 1;

  
11: var actualRead = stream.EndRead(asyncResult2);

  
12: }

  
13: }

  Continue方法的定义是:

  1: public class Context

  
2: {

  
3: //...

  
4: private IEnumerator enumerator;

  
5:

  
6: public AsyncCallback Continue()

  
7: {

  
8: return (ar) => enumerator.MoveNext();

  
9: }

  
10: }

  在调用Continue方法之前,Context类还必须保存有Download方法返回的IEnumerator,所以:

  1: public class Context

  
2: {

  
3: //...

  
4: private IEnumerator enumerator;

  
5:

  
6: public AsyncCallback Continue()

  
7: {

  
8: return (ar) => enumerator.MoveNext();

  
9: }

  
10:

  
11: public void Run(IEnumerator enumerator)

  
12: {

  
13: this.enumerator = enumerator;

  
14: enumerator.MoveNext();

  
15: }

  
16: }

  那调用Download的方法就可以写成:

  1: public void Main()

  
2: {

  
3: Program p = new Program();

  
4:

  
5: Context context = new Context();

  
6: context.Run(p.Download(context));

  
7: }

  除了执行方式的不同外,我们几乎就可以像同步的方式那样编写异步的代码了。

  完整的代码如下(为了更好的演示,我将下面代码改为Winform版本):

  1: public class Context

  
2: {

  
3: private IEnumerator enumerator;

  
4:

  
5: public AsyncCallback Continue()

  
6: {

  
7: return (ar) => enumerator.MoveNext();

  
8: }

  
9:

  
10: public void Run(IEnumerator enumerator)

  
11: {

  
12: this.enumerator = enumerator;

  
13: enumerator.MoveNext();

  
14: }

  
15: }

  
16:

  
17: private void btnDownload_click(object sender,EventArgs e)

  
18: {

  
19: Context context = new Context();

  
20: context.Run(Download(context));

  
21: }

  
22:

  
23: private IEnumerator Download(Context context)

  
24: {

  
25: var request = HttpWebRequest.Create("http://www.google.com");

  
26: var asyncResult1 = request.BeginGetResponse(context.Continue(),null);

  
27: yield return 1;

  
28: var response = request.EndGetResponse(asyncResult1);

  
29: using(stream = response.GetResponseStream())

  
30: {

  
31: var asyncResult2 = stream.BeginRead(buffer, 0, 1024, context.Continue(),null);

  
32: yield return 1;

  
33: var actualRead = stream.EndRead(asyncResult2);

  
34: }

  
35: }

  不知道你注意到没有,我们不仅可以顺序的编写异步代码,连using这样的构造也可以使用了。如果你想更深入的理解这段代码,推荐你使用Reflector查看迭代器最后生成的代码。我在这里做一下简短的描述:

  1、Context的Run调用时会调用Dowload方法,得到一个IEnumerator对象,我们将该对象保存在Context的实例字段中,以备后用

  2、调用该IEnumerator对象的MoveNext方法,该方法会执行到第一个yield return位置,然后返回,这个时候request.BeginGetResponse已经调用,这个时候线程可以干其他的事情了。

  3、在BeginGetResponse调用时我们通过Context的Continue方法传入了一个回调,该回调里会执行刚才保存的IEnumerator对象的MoveNext方法。也就是在BeginGetResponse这个异步请求执行完毕后,会调用MoveNext方法,控制流又回到Download方法,执行到下一个yield return…… 以此类推。

  总结

  总结本文,我们发现我们要的东西就是怎样将顺序风格的代码转换为CPS方式,如何去寻找发起异步请求这行代码的continue。由于C#提供了yield这种机制,C#编译器会为其生产一个状态机,能够将控制权在调用代码和被调用代码之间交换。

  要注意的是本文最后实现的异步执行方式是非常简陋的,绝对不能应用在产品代码上。这里仅仅是为了演示目的。在这方面微软社区的大牛Jeffrey Ritcher早以为我们开发了Power Threading这个类库,里面提供了AsyncEnumerator类,是一种更可靠的实现。

  而微软自己为机器人开发提供的CCR也提供了相类似的实现。我们会在下一篇文章来学习这两个类库。

0
相关文章