一起谈.NET技术,.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.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<int> 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(
时间: 2024-10-22 07:08:08

一起谈.NET技术,.NET中的异步编程-Continuation passing style以及使用yield实现异步的相关文章

.NET中的异步编程-Continuation passing style以及使用yield实现异步

传统的异步方式将本来紧凑的代码都分成两部分,不仅仅降低了代码的可读性,还让一些基本的程序构造无法使用,所以大部分开发人员在遇到应该使用异步的地方都忍痛割爱.本来我在本篇文章中想讨论一下.NET世界中已有的几个辅助异步开发的类库,但是经过思考后觉得在这之前介绍一下一些理论知识也许对理解后面的类库以及更新的内容有所帮助.今天我们要讨论的是Continuation Passing Style,简称CPS. CPS 首先,我们看看下面这个方法: 1: public int Add(int a, int

.NET中的异步编程-Continuation passing style以及使用yield实现异“.NET研究”步

传统的异步方式将本来紧凑的代码都分成两部分,不仅仅降低了代码的可读性,还让一些基本的程序构造无法使用,所以大部分开发人员在遇到应该使用异步的地方都忍痛割爱.本来我在本篇文章中想讨论一下.NET世界中已有的几个辅助异步开发的类库,但是经过思考后觉得在这之前介绍一下一些理论知识也许对理解后面的类库以及更新的内容有所帮助.今天我们要讨论的是Continuation Passing Style,简称CPS. CPS 首先,我们看看下面这个方法: 1: public int Add(上海企业网站设计与制作

一起谈.NET技术,中软面试题-最新

      中软的面试比较经典,也比较严格,一般有四轮,类似于微软的面试.中软面过以后,根据项目组,会推到美国微软那边运用live meeting & con-call 再面一次.以下是我的面试题及个人的小分析,拿出来和大家share一下.希望更多的人能过这个坎.如有什么问题,可以一起交流.直接进入主题:  1. English communication. (sale yourself, project information, your interesting,and how to deal

一起谈.NET技术,.Net4.0 Parallel编程(二)Data Parallelism 中

在上篇文章中看过了使用Parrallel.For.Parael.Foreach在效率上给我们带来的提高.本文就来如何终止循环.线程局部变量 进行说明. Thread-Local Variables 首先我们来看下线程局部变量,是的我们也许一直在想我们如何去定义一个线程局部变量呢.先看段顺序执行的代码: [TestMethod()]public void NormalSequenceTest(){int[] nums = Enumerable.Range(0, 1000000).ToArray()

一起谈.NET技术,C# 4动态编程新特性与DLR剖析

近几年来,在TIOBE 公司每个月发布的编程语言排行榜 [1] 中,C# 总是能挤进前10 名,而在近10 年的编程语言排行榜中,C# 总体上呈现上升的趋势.C# 能取得这样的成绩,有很多因素在起作用,其中,它在语言特性上的锐意进取让人印象深刻( 图1 ). 图1 C#各版本的创新点 2010 年发布的 C# 4 ,最大的创新点是拥有了动态编程语言的特性. 1 动态编程语言的中兴 动态编程语言并非什么新鲜事物,早在面向对象编程语言成为主流之前,人们就已经使用动态编程语言来开发了.即使在 Java

一起谈.NET技术,Powershell简介及其编程访问

这个工具可以单独使用,完全可以取代cmd.exe.例如如下: 但它的功能远不止于此,例如我们可以很容易地获取所有的进程名称: 再来看一个,下面这个例子是获取当前正在运行的服务列表.(可以用条件很方便地筛选): 除此之外,Powershell还支持定制,例如微软很多产品都提供了专门的Powershell插件(典型的有:SQL Server,SharePoint Server, Exchange Server等).通过这些特殊的外壳,可以实现对服务器的管理.功能非常强大.例如下面的SQLPS,可以像

一起谈.NET技术,.NET并行(多核)编程系列之七 共享数据问题和解决概述

之前的文章介绍了了并行编程的一些基础的知识,从本篇开始,将会讲述并行编程中实际遇到一些问题,接下来的几篇将会讲述数据共享问题. 本篇的议题如下: 1.数据竞争 2.解决方案提出 3.顺序的执行解决方案 4.数据不变解决方案 在开始之前,首先,我们来看一个很有趣的例子: class BankAccount { public int Balance { get; set; } } class App { static void Main(string[] args) { // create the

一起谈.NET技术,.Net4.0 Parallel编程(一)Data Parallelism 上

Parallel.For 首先先写一个普通的循环: private void NormalFor(){for (var i = 0; i < 10000; i++) {for (var j = 0; j < 1000; j++) {for (var k = 0; k < 100; k++) { DoSomething(); } } }} 再看一个并行的For语句: private void ParallelFor(){ Parallel.For(0, 10000, i => {fo

一起谈.NET技术,10个C#编程和Visual Studio使用技巧

C#是一门伟大的编程语言,与C++和Java相比,它的语法更简单,相对来说更好入门,经历10年的发展,C#已经成为编程语言领域强有力的竞争者,每一年我们都能看到它的进步,每一个新版本都加入了许多新特性,总的来说,作为一门编程语言,它没有让C#开发者社区失望.Visual Studio亦是如此,新版本的Visual Studio 2010所带来的新特性也让开发者们兴奋不已. 对开场白没兴趣?好吧,我们直接切入正题,下面介绍10个C#编程和Visual Studio IDE使用技巧. 1.Envir