加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 编程开发 > asp.Net > 正文

ASP.NET MVC下的异步Action的定义和执行原理

发布时间:2020-12-16 09:05:30 所属栏目:asp.Net 来源:网络整理
导读:Visual Studio提供的Controller创建向导默认为我们创建一个继承自抽象类Controller的Controller类型,这样的Controller只能定义同步Action方法。如果我们需要定义异步Action方法,必须继承抽象类AsyncController。这篇问你讲述两种不同的异步Action的定义方

Visual Studio提供的Controller创建向导默认为我们创建一个继承自抽象类Controller的Controller类型,这样的Controller只能定义同步Action方法。如果我们需要定义异步Action方法,必须继承抽象类AsyncController。这篇问你讲述两种不同的异步Action的定义方法和底层执行原理。[本文已经同步到《How ASP.NET MVC Works?》中]

目录
一、基于线程池的请求处理
二、两种异步Action方法的定义
??? XxxAsync/XxxCompleted
??? Task返回值
三、AsyncManager
四、Completed方法的执行
五、异步操作的超时控制

一、基于线程池的请求处理

ASP.NET通过线程池的机制处理并发的HTTP请求。一个Web应用内部维护着一个线程池,当探测到抵达的针对本应用的请求时,会从池中获取一个空闲的线程来处理该请求。当处理完毕,线程不会被回收,而是重新释放到池中。线程池具有一个线程的最大容量,如果创建的线程达到这个上限并且所有的线程均被处于“忙碌”状态,新的HTTP请求会被放入一个请求队列以等待某个完成了请求处理任务的线程重新释放到池中。

我们将这些用于处理HTTP请求的线程称为工作线程(Worker Thread),而这个县城池自然就叫做工作线程池。ASP.NET这种基于线程池的请求处理机制主要具有如下两个优势:

  • 工作线程的重用:创建线程的成本虽然不如进程的激活,却也不是一件“一蹴而就”的事情,频繁地创建和释放线程会对性能造成极大的损害。而线程池机制避免了总是创建新的工作线程来处理每一个请求,被创建的工作线程得到了极大地重用,并最终提高了服务器的吞吐能力。
  • 工作线程数量的限制:资源的有限性具有了服务器处理请求的能力具有一个上限,或者说某台服务器能够处理的请求并发量具有一个临界点,一旦超过这个临界点,整台服务将会因不能提供足够的资源而崩溃。由于采用了对工作线程数量具有良好控制的线程池机制,ASP.NET MVC并发处理的请求数量不可能超过线程池的最大允许的容量,从而避免了在高并发情况下工作线程的无限制创建而最导致整个服务器的崩溃。

如果请求处理操作耗时较短,那么工作线程处理完毕后可以及时地被释放到线程池中以用于对下一个请求的处理。但是对于比较耗时的操作来说,意味着工作线程将被长时间被某个请求独占,如果这样的操作访问比较频繁,在高并发的情况下意味着线程池中将可能找不到空闲的工作线程用于及时处理最新抵达请求。

如果我们采用异步的方式来处理这样的耗时请求,工作线程可以让后台线程来接手,自己可以及时地被释放到线程池中用于进行后续请求的处理,从而提高了整个服务器的吞吐能力。值得一提的是,异步操作主要用于I/O绑定操作(比如数据库访问和远程服务调用等),而非CPU绑定操作,因为异步操作对整体性能的提升来源于:当I/O设备在处理某个任务的时候,CPU可以释放出来处理另一个任务。如果耗时操作主要依赖于本机CPU的运算,采用异步方法反而会因为线程调度和线程上下文的切换而影响整体的性能。

二、两种异步Action方法的定义

在了解了在AsyncController中定义异步Action方法的必要性之后,我们来简单介绍一下异步Action方法的定义方式。总的来说,异步Action方法具有两种定义方式,一种是将其定义成两个匹配的方法XxxAsync/XxxCompleted,另一种则是定义一个返回类型为Task的方法。

XxxAsync/XxxCompleted

如果我们使用两个匹配的方法XxxAsync/XxxCompleted来定义异步Action,我们可以将异步操作实现在XxxAsync方法中,而将最终内容的呈现实现在XxxCompleted方法中。XxxCompleted可以看成是针对XxxAsync的回调,当定义在XxxAsync方法中的操作以异步方式执行完成后,XxxCompleted方法会被自动调用。XxxCompleted的定义方式和普通的同步Action方法比较类似。

作为演示,我在如下一个HomeController中定义了一个名为Article的异步操作来呈现指定名称的文章内容。我们将指定文章内容的异步读取定义在ArticleAsync方法中,而在ArticleCompleted方法中讲读取的内容以ContentResult的形式呈现出来。

   1: public class HomeController : AsyncController
   3:     void ArticleAsync(string name)
   5:         AsyncManager.OutstandingOperations.Increment();
   7:             {
   9:                 using (StreamReader reader = new StreamReader(path))
  11:                     AsyncManager.Parameters["content"] = reader.ReadToEnd();
  13:                 AsyncManager.OutstandingOperations.Decrement();
  15:     }
  17:     {
  19:     }
    
   6:             {
new StreamReader(path))
  10:                     AsyncManager.Parameters["content"] = reader.ReadToEnd();
  12:             }).ContinueWith<ActionResult>(task =>
  14:                     string content = (string)AsyncManager.Parameters["content"];
  16:                 });
  18: }

上面定义的异步Action方法Article的返回类型为Task<ActionResult>,我们将异步文件内容的读取体现在返回的Task对象中。对文件内容呈现的回调操作则通过调用该Task对象的ContinueWith<ActionResult>方法进行注册,该操作会在异步操作完成之后被自动调用。

如上面的代码片断所示,我们依然利用AsyncManager的Parameters属性实现参数在异步操作和回调操作之间的传递。其实我们也可以使用Task对象的Result属性来实现相同的功能,Article方法的定义也改写成如下的形式。

13: {
  15:                 });
  17: }

三、AsyncManager

在上面演示的异步Action的定义中,我们通过AsyncManager实现了两个基本的功能,即在异步操作和回调操作之间传递参数和向ASP.NET MVC发送异步操作开始和结束的通知。由于AsyncManager在异步Action场景中具有重要的作用,我们有必要对其进行单独介绍,下面是AsyncManager的定义。

2: {
   4:     public AsyncManager(SynchronizationContext syncContext);
   6:     public EventHandler Finished;
   8:     virtual void Finish();
  10:     
  12:     public IDictionary<string,object> Parameters { get; }
  14: }
  16: sealed class OperationCounter
  18:     event EventHandler Completed;    
  20:     int Increment();
  22:     int Decrement();
  25:     int Count { get; }
   1: AsyncManager.OutstandingOperations.Increment(3);
   3: Task.Factory.StartNew(() =>
   5:     //异步操作1
   7: });
   9: {
  11:     AsyncManager.OutstandingOperations.Decrement();
  13: Task.Factory.StartNew(() =>
  15:     //异步操作3
  17: });

对于每次通过Increment和Decrement方法调用引起的计数数值的改变,OperationCounter对象都会检验当前计数数值是否为零,如果则表明所有的操作运行完毕,如果预先注册了Completed事件,该事件会被触发。值得一提的时候,表明所有操作完成执行的标志是计数器的值等于零,而不是小于零,如果我们通过调用Increment和Decrement方法使计数器的值称为一个负数,注册的Completed事件是不会被触发的。

AsyncManager在初始化的时候就注册了通过属性OutstandingOperations表示的OperationCounter对象的Completed事件,使该事件触发的时候调用自身的Finish方法。而虚方法Finish在AsyncManager中的默认实现又会触发自身的Finished事件。

如下面的代码片断所示,Controller类实现了IAsyncManagerContainer接口,而后者定义了一个只读属性AsyncManager用于提供辅助执行异步Action的AsyncManager对象,而我们在定义异步Action方法是使用的AsyncManager对象就是从抽象类Controller中集成下来的AsyncManager属性。

public AsyncManager AsyncManager { get; }
   6: interface IAsyncManagerContainer
   8:     AsyncManager AsyncManager { get; }
   6:     AsyncManager.Finish();
  16:     AsyncManager.Finish();
//AsyncManager.OutstandingOperations.Increment();
  20: }

实际上ArticleCompleted依然会被执行,但是这样我们就不能确保正常读取文章内容,因为ArticleCompleted方法会在ArticleAsync方法执行之后被立即执行。如果文章内容读取是一个相对耗时的操作,表示文章内容的ArticleCompleted方法的content参数在执行的时候尚未被初始化。在这种情况下的ArticleCompleted是如何被执行的呢?

原因和简单,ReflectedAsyncActionDescriptor的BeginExecute方法在执行XxxAsync方法的前后会分别调用AsyncManager的OutstandingOperations属性的Increment和Decrement方法。对于我们给出的例子来说,在执行ArticleAsync之前Increment方法被调用使计算器的值变成1,随后ArticleAsync被执行,由于该方法以异步的方式读取指定的文件内容,所以会立即返回。最后Decrement方法被执行使计数器的值变成0,AsyncManager的Completed事件被触发并导致ArticleCompleted方法的执行。而此时,文件内容的读取正在进行之中,表示文章内容的content参数自然尚未被初始化。

ReflectedAsyncActionDescriptor这样的执行机制也对我们使用AsyncManager提出了要求,那就是对尚未完成的一步操作计数器的增加操作不应该发生在异步线程中,如下所示的针对AsyncManager的OutstandingOperations属性的Increment方法的定义是不对的。

7: AsyncManager.OutstandingOperations.Increment();
   9:                   AsyncManager.OutstandingOperations.Decrement();
  11:     }
  13: } 

下面采用正确的定义方法:

5: AsyncManager.OutstandingOperations.Increment();
  13: }  

最后再强调一点,不论是显式调用AsyncManager的Finish方法,还是通过调用AsyncManager的OutstandingOperations属性的Increment方法是计数器的值变成零,仅仅是让XxxCompleted方法得以执行,并不能真正阻止异步操作的执行。

五、异步操作的超时控制

异步操作虽然适合那些相对耗时的I/O绑定型操作,但是也并不说对一步操作执行的时间没有限制。异步超时时限通过AsyncManager的整型属性Timeout表示,它表示超时时限的总毫秒数,其默认值为45000(45秒)。如果将Timeout属性设置为-1,意味着异步操作执行不再具有任何时间的限制。对于以XxxAsync/XxxCompleted形式定义的异步Action来说,如果XxxAsync执行之后,在规定的超时时限中XxxCompleted没有得到执行,一个TimeoutException会被抛出来。

如果我们以返回类型为Task的形式定义异步Action,通过Task体现的异步操作的执行时间不受AsyncManager的Timeout属性的限制。我们通过如下的代码定义了一个名为Data的异步Action方法以异步的方式获取作为Model的数据并通过默认的View呈现出来,但是异步操作中具有一个无限循环,当我们访问该Data方法时,异步操作将会无限制地执行下去,也不会有TimeoutException异常发生。