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

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

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

  CPS

  首先,我们看看下面这个方法:

   1: public int Add(上海企业网站设计与制作style="color: #0000ff;">int a, int b)
   2: {
   3:     return a + b;
   4: }

  我们一般这样调用它:

   1: Print(Add(5, 6))
   2:  
   3: public void Print(int result)
上海闵行企业网站设计与制作 style="color: #606060;">   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(object sender,EventArgs e)
  18: {
  19:     Context context = new Context();
  20:     context.Run(Download(context));
  21: }
  22:  
  23: private IEnumerator<int> 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也提供了相类似的实现。我们会在下一篇文章来学习这两个类库。

目录
相关文章
|
4月前
|
数据库 开发者
.NET 异步编程之谜:async/await 模式究竟隐藏着怎样的神奇力量?
【8月更文挑战第28天】在当今注重效率和响应性的软件开发领域,.NET 的 async/await 模式如同得力助手,简化异步代码编写,使代码更易理解和维护。通过后台执行耗时操作,如网络请求和数据库查询,避免阻塞主线程,显著提升系统响应性。此模式不仅适用于网络请求,还广泛应用于数据库操作和文件读写。合理使用 async/await 可大幅优化性能,但需注意避免过度使用、正确处理调用链及异常,以确保系统稳定性和高效性。深入探索 async/await,助您构建更出色的应用程序。
59 0
|
1月前
|
开发框架 Java .NET
.net core 非阻塞的异步编程 及 线程调度过程
【11月更文挑战第12天】本文介绍了.NET Core中的非阻塞异步编程,包括其基本概念、实现方式及应用示例。通过`async`和`await`关键字,程序可在等待I/O操作时保持线程不被阻塞,提高性能。文章还详细说明了异步方法的基础示例、线程调度过程、延续任务机制、同步上下文的作用以及如何使用`Task.WhenAll`和`Task.WhenAny`处理多个异步任务的并发执行。
|
4月前
分享一份 .NET Core 简单的自带日志系统配置,平时做一些测试或个人代码研究,用它就可以了
分享一份 .NET Core 简单的自带日志系统配置,平时做一些测试或个人代码研究,用它就可以了
|
6月前
|
机器学习/深度学习 JSON 测试技术
CNN依旧能战:nnU-Net团队新研究揭示医学图像分割的验证误区,设定先进的验证标准与基线模型
在3D医学图像分割领域,尽管出现了多种新架构和方法,但大多未能超越2018年nnU-Net基准。研究发现,许多新方法的优越性未经严格验证,揭示了验证方法的不严谨性。作者通过系统基准测试评估了CNN、Transformer和Mamba等方法,强调了配置和硬件资源的重要性,并更新了nnU-Net基线以适应不同条件。论文呼吁加强科学验证,以确保真实性能提升。通过nnU-Net的变体和新方法的比较,显示经典CNN方法在某些情况下仍优于理论上的先进方法。研究提供了新的标准化基线模型,以促进更严谨的性能评估。
177 0
|
6月前
|
设计模式 存储 编译器
【.NET Core】异步编程模式
【.NET Core】异步编程模式
58 2
|
6月前
|
SQL 设计模式 开发框架
.NET异步有多少种实现方式?(异步编程提高系统性能、改善用户体验)
想要知道.NET异步有多少种实现方式,首先我们要知道.NET提供的执行异步操作的三种模式,然后再去了解.NET异步实现的四种方式。
|
6月前
|
开发框架 .NET 对象存储
【.NET Core】深入理解异步编程模型(APM)
【.NET Core】深入理解异步编程模型(APM)
138 1
|
7月前
|
机器学习/深度学习 算法 数据可视化
MATLAB基于深度学习U-net神经网络模型的能谱CT的基物质分解技术研究
MATLAB基于深度学习U-net神经网络模型的能谱CT的基物质分解技术研究
|
机器学习/深度学习 数据采集 存储
【3-D深度学习:肺肿瘤分割】创建和训练 V-Net 神经网络,并从 3D 医学图像中对肺肿瘤进行语义分割研究(Matlab代码实现)
【3-D深度学习:肺肿瘤分割】创建和训练 V-Net 神经网络,并从 3D 医学图像中对肺肿瘤进行语义分割研究(Matlab代码实现)
274 0