.NET中的异步编程(三)- Continuation passing style以及使用yield实现异步
在上一篇文章中我们围观了传统的异步编程,感受到了异步编程不是简单的事情。传统的异步方式将本来紧凑的代码都分成两部分,不仅仅降低了代码的可读性,还让一些基本的程序构造无法使用,所以大部分开发人员在遇到应该使用异步的地方都忍痛割爱。本来我在本篇文章中想讨论一下.NET世界中已有的几个辅助异步开发的类库,但是经过思考后觉得在这之前介绍一下一些理论知识也许对理解后面的类库以及更新的内容有所帮助。今天我们要讨论的是Continuation Passing Style,简称CPS
。
CPS
首先,我们看看下面这个方法:1: public int Add(int a, int b)
2: {
3: return a + b;
4: }我们一般这样调用它:
1: Print(Add(5, 6))
2:
3: public void Print(int result)
4: {
5: Console.WriteLine(result);
6: }如果我们以CPS的方式编写上面的代码则是这个样子:
1: public void Add(int a, int b, Action<int> continueWith)
2: {
3: continueWith(a+b);
4: }
5:
6: Add(5, 6, (ret) => Print(ret));就好像我们将方法倒过来,我们不再是直接返回方法的结果;我们现在做的是接受一个委托,这个委托表示我这个方法运算完后要干什么,就是传说的continue。对于这里来说,Add的continue就是Print。
不仅是上面这样的代码示例。在一个方法中,在本语句后面执行的语句都可以称之为本语句的continue。
CPS 与 Async
那么可能有人要问,你说这么多跟异步有什么关系么?对,跟异步有很大的关系。回想上一篇文章,经典的异步模式都是一个以Begin开头的方法发起异步请求,并且向这个方法传入一个回调(callback),当异步执行完毕后该回调会被执行,那么我们可以称该回调为这个异步请求的continue:1: stream.BeginRead(buffer, 0, 1024, continueWith, null)
这又有什么用呢?那先来看看我们期望写出什么样子的异步代码吧(注意,这是伪代码,不要没有看文章就直接粘贴代码到vs运行):
1: var request = HttpWebRequest.Create("http://www.google.com");
2: var asyncResult1 = request.BeginGetResponse(...);
3: var response = request.EndGetResponse(asyncResult1);
4: using(stream = response.GetResponseStream())
5: {
6: var asyncResult2 = stream.BeginRead(buffer, 0, 1024, ...);
7: var actualRead = stream.EndRead(asyncResult2);
8: }对,我们想要像同步的方式一样编写异步代码,我讨厌那么多回调,特别是一环嵌套一环的回调。
参照前面对CPS的讨论,在request.BeginGetResponse之后的代码,都是它的continue,如果我能够有一种机制获得我的continue,然后在我执行完毕之后调用continue该多好啊。可惜,C#没有像Scheme那样的控制操作符call/cc获取continue。
思路貌似到这儿断了。但是我们是否可以换个角度想想,如果我们能给上面这段代码加上标识:在每个异步请求发起的地方都加一个标识,而标识之后的部分就是continue。
var request = HttpWebRequest.Create("http://www.google.com");
标识1 var asyncResult1 = request.BeginGetResponse(...);
var response = request.EndGetResponse(asyncResult1);
using(stream = response.GetResponseStream())
{
标识2 var asyncResult2 = stream.BeginRead(buffer, 0, 1024, ...);
var actualRead = stream.EndRead(asyncResult2);
}当执行到 标识1 时,立即返回,并且记住本次执行只执行到了 标识1,当异步请求完毕后,它知道上次执行到了 标识1,那么这个时候就从标识1的下一行开始执行,当执行到标识2时,又遇到一个异步请求,立即返回并记住本次执行到了标识2,然后请求完毕后从标识2的下一行恢复执行。那么现在的任务就是如果打标识以及在异步请求完毕后如何从标识位置开始恢复执行。
yield 与 异步
如果你熟悉C# 2.0加入的迭代器特性,你就会发现yield就是我们可以用来打标识的东西。看下面的代码:1: public IEnumerator<int> 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<int> 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<int> Download()
2: {
3: var request = HttpWebRequest.Create("http://www.google.co
补充:Web开发 , ASP.Net ,