yield 与 异步
如果你熟悉C# 2.0加入的迭代器特性,你就会发现yield就是我们可以用来打标识的东西。看下面的代码:
2: {
3: //code 1
4: yield return 1;
5: //code 2
6: yield return 2;
7: //code 3
8: yield return 3;
9: }
经过编译会生成类似下面的代码(伪代码,相差很远,只是意义相近,想要了解详情的同学可以自行打开Reflector观看):
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就行了,那个状态机会帮我们处理一切。
那我们改造我们的异步代码:
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怎么样?
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方法的定义是:
2: {
3: //...
4: private IEnumerator enumerator;
5:
6: public AsyncCallback Continue()
7: {
8: return (ar) => enumerator.MoveNext();
9: }
10: }
在调用Continue方法之前,Context类还必须保存有Download方法返回的IEnumerator,所以:
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的方法就可以写成:
2: {
3: Program p = new Program();
4:
5: Context context = new Context();
6: context.Run(p.Download(context));
7: }
除了执行方式的不同外,我们几乎就可以像同步的方式那样编写异步的代码了。
完整的代码如下(为了更好的演示,我将下面代码改为Winform版本):
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也提供了相类似的实现。我们会在下一篇文章来学习这两个类库。