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

Visual Studio提供的Controller创建向导默认为我们创建一个继承自抽象类Controller的Controller类型,这样的 Controller只能定义同步Action方法。如果我们需要定义异步Action方法,必须继承抽象类AsyncController。这篇问你讲 述两种不同的异步Action的定义方法和底层执行原理。

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

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

1:publicclass HomeController : AsyncController

 

双击代码全选

1

2: {

 

双击代码全选

1

3:publicvoid ArticleAsync(string name)

 

双击代码全选

1

4:     {

 

双击代码全选

1

5:         AsyncManager.OutstandingOperations.Increment();

 

双击代码全选

1

6:         Task.Factory.StartNew(() =>

 

双击代码全选

1

7:             {

 

双击代码全选

1

8:string path = ControllerContext.HttpContext.Server.MapPath(string.Format(@"articles{0}.html", name));

 

双击代码全选

1

9:using (StreamReader reader = new StreamReader(path))

 

双击代码全选

1

10:                 {

 

双击代码全选

1

11:                     AsyncManager.Parameters["content"] = reader.ReadToEnd();

 

双击代码全选

1

12:                 }

 

双击代码全选

1

13:                 AsyncManager.OutstandingOperations.Decrement();

 

双击代码全选

1

14:             });

 

双击代码全选

1

15:     }

 

双击代码全选

1

16:public ActionResult ArticleCompleted(string content)

 

双击代码全选

1

17:     {

 

双击代码全选

1

18:return Content(content);

 

双击代码全选

1

19:     }

 

双击代码全选

1

20: }

 

对 于以XxxAsync/XxxCompleted形式定义的异步Action方法来说,ASP.NET MVC并不会以异步的方式来调用XxxAsync方法,所以我们需要在该方法中自定义实现异步操作的执行。在上面定义的ArticleAsync方法中, 我们是通过基于Task的并行编程方式来实现对文章内容的异步读取的。当我们以XxxAsync/XxxCompleted形式定义的异步Action方 法的时候,会频繁地使用到Controller的AsyncManager属性,该属性返回一个类型为AsyncManager对象,我们将在下面一节对 其进行单独讲述。
在上面提供的实例中,我们在异步操作开始和结束的时候调用了AsyncManager的 OutstandingOperations属性的Increment和Decrement方法对于ASP.NET MVC发起通知。此外,我们还利用AsyncManager的Parameters属性表示的字典来保存传递给ArticleCompleted方法的参 数,参数在字典中的Key(content)与ArticleCompleted的参数名称是匹配的,所以在调用方法ArticleCompleted的 时候,通过AsyncManager的Parameters属性指定的参数值将自动作为对应的参数值。

Task返回值

如 果采用上面的异步Action定义方式,意味着我们不得不为一个Action定义两个方法,实际上我们可以通过一个方法来完成对异步Action的定义, 那就是让Action方法返回一个代表异步操作的Task对象。上面通过XxxAsync/XxxCompleted形式定义的异步Action可以采用 如下的定义方式。

双击代码全选

1

1:publicclass HomeController : AsyncController

 

双击代码全选

1

2: {

 

双击代码全选

1

3:public Task<ActionResult> Article(string name)

 

双击代码全选

1

4:     {

 

双击代码全选

1

5:return Task.Factory.StartNew(() =>

 

双击代码全选

1

6:             {

 

双击代码全选

1

7:string path = ControllerContext.HttpContext.Server.MapPath(string.Format(@"articles{0}.html", name));

 

双击代码全选

1

8:using (StreamReader reader = new StreamReader(path))

 

双击代码全选

1

9:                 {

 

双击代码全选

1

10:                     AsyncManager.Parameters["content"] = reader.ReadToEnd();

 

双击代码全选

1

11:                 }

 

双击代码全选

1

12:             }).ContinueWith<ActionResult>(task =>

 

双击代码全选

1

13:                 {

 

双击代码全选

1

14:string content = (string)AsyncManager.Parameters["content"];

 

双击代码全选

1

15:return Content(content);

 

双击代码全选

1

16:                 });

 

双击代码全选

1

17:     }

 

双击代码全选

1

18: }

 

上 面定义的异步Action方法Article的返回类型为Task<ActionResult>,我们将异步文件内容的读取体现在返回的 Task对象中。对文件内容呈现的回调操作则通过调用该Task对象的ContinueWith<ActionResult>方法进行注册, 该操作会在异步操作完成之后被自动调用。
如上面的代码片断所示,我们依然利用AsyncManager的Parameters属性实现参数在异步操作和回调操作之间的传递。其实我们也可以使用Task对象的Result属性来实现相同的功能,Article方法的定义也改写成如下的形式。