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

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

MVC下的异步Action的定义和执⾏原理
Visual Studio提供的Controller创建向导默认为我们创建⼀个继承⾃抽象类Controller的Controller类型,这样的Controller只能定义同步Action⽅法。

如果我们需要定义异步Action⽅法,必须继承抽象类AsyncController。

这篇问你讲述两种不同的异步Action的定义⽅法和底层执⾏原理。

[本⽂已经同步到《》中]
⽬录
⼀、基于线程池的请求处理
⼆、两种异步Action⽅法的定义
XxxAsync/XxxCompleted
Task返回值
三、AsyncManager
四、Completed⽅法的执⾏
五、异步操作的超时控制
⼀、基于线程池的请求处理
通过线程池的机制处理并发的HTTP请求。

⼀个Web应⽤内部维护着⼀个线程池,当探测到抵达的针对本应⽤的请求时,会从池中获取⼀个空闲的线程来处理该请求。

当处理完毕,线程不会被回收,⽽是重新释放到池中。

线程池具有⼀个线程的最⼤容量,如果创建的线程达到这个上限并且所有的线程均被处于“忙碌”状态,新的HTTP 请求会被放⼊⼀个请求队列以等待某个完成了请求处理任务的线程重新释放到池中。

我们将这些⽤于处理HTTP请求的线程称为⼯作线程(Worker Thread),⽽这个县城池⾃然就叫做⼯作线程池。

这种基于线程池的请求处理机制主要具有如下两个优势:
⼯作线程的重⽤:创建线程的成本虽然不如进程的激活,却也不是⼀件“⼀蹴⽽就”的事情,频繁地创建和释放线程会对性能造成极⼤的损害。

⽽线程池机制避免了总是创建新的⼯作线程来处理每⼀个请求,被创建的⼯作线程得到了极⼤地重⽤,并最终提⾼了服务器的吞吐能⼒。

⼯作线程数量的限制:资源的有限性具有了服务器处理请求的能⼒具有⼀个上限,或者说某台服务器能够处理的请求并发量具有⼀个临界点,⼀旦超过这个临界点,整台服务将会因不能提供⾜够的资源⽽崩溃。

由于采⽤了对⼯作线程数量具有良好控制的线程池机制, 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
2: {
3:public void ArticleAsync(string name)
4: {
5: AsyncManager.OutstandingOperations.Increment();
6: Task.Factory.StartNew(() =>
7: {
8:string path = ControllerContext.HttpContext.Server.MapPath(string.Format(@"\articles\{0}.html", name));
9:using (StreamReader reader = new StreamReader(path))
10: {
11: AsyncManager.Parameters["content"] = reader.ReadToEnd();
12: }
13: AsyncManager.OutstandingOperations.Decrement();
14: });
15: }
16:public ActionResult ArticleCompleted(string content)
17: {
18:return Content(content);
19: }
20: }
对于以XxxAsync/XxxCompleted形式定义的异步Action⽅法来说, MVC并不会以异步的⽅式来调⽤XxxAsync⽅法,所以我们需要在该⽅法中⾃定义实现异步操作的执⾏。

在上⾯定义的ArticleAsync⽅法中,我们是通过基于Task的并⾏编程⽅式来实现对⽂章内容的异步读取的。

当我们以XxxAsync/XxxCompleted形式定义的异步
Action⽅法的时候,会频繁地使⽤到Controller的AsyncManager属性,该属性返回⼀个类型为对象,我们将在下⾯⼀节对其进⾏单独讲述。

在上⾯提供的实例中,我们在异步操作开始和结束的时候调⽤了AsyncManager的OutstandingOperations属性的Increment和Decrement⽅法对于 MVC发起通知。

此外,我们还利⽤AsyncManager的Parameters属性表⽰的字典来保存传递给ArticleCompleted⽅法的参数,参数在字典中的Key(content)与ArticleCompleted的参数名称是匹配的,所以在调⽤⽅法ArticleCompleted的时候,通过AsyncManager的Parameters属性指定的参数值将⾃动作为对应的参数值。

Task返回值
如果采⽤上⾯的异步Action定义⽅式,意味着我们不得不为⼀个Action定义两个⽅法,实际上我们可以通过⼀个⽅法来完成对异步Action的定义,那就是让Action⽅法返回⼀个代表异步操作的Task对象。

上⾯通过XxxAsync/XxxCompleted形式定义的异步Action可以采⽤如下的定义⽅式。

1:public class HomeController : AsyncController
2: {
3:public Task<ActionResult> Article(string name)
4: {
5:return Task.Factory.StartNew(() =>
6: {
7:string path = ControllerContext.HttpContext.Server.MapPath(string.Format(@"\articles\{0}.html", name));
8:using (StreamReader reader = new StreamReader(path))
9: {
10: AsyncManager.Parameters["content"] = reader.ReadToEnd();
11: }
12: }).ContinueWith<ActionResult>(task =>
13: {
14:string content = (string)AsyncManager.Parameters["content"];
15:return Content(content);
16: });
17: }
18: }
上⾯定义的异步Action⽅法Article的返回类型为Task<ActionResult>,我们将异步⽂件内容的读取体现在返回的Task对象中。

对⽂件内容呈现的回调操作则通过调⽤该Task 对象的ContinueWith<ActionResult>⽅法进⾏注册,该操作会在异步操作完成之后被⾃动调⽤。

如上⾯的代码⽚断所⽰,我们依然利⽤AsyncManager的Parameters属性实现参数在异步操作和回调操作之间的传递。

其实我们也可以使⽤Task对象的Result属性来实现相同的功能,Article⽅法的定义也改写成如下的形式。

1:public class HomeController : AsyncController
2: {
3:public Task<ActionResult> Article(string name)
4: {
5:return Task.Factory.StartNew(() =>
6: {
7:string path = ControllerContext.HttpContext.Server.MapPath(string.Format(@"\articles\{0}.html", name));
8:using (StreamReader reader = new StreamReader(path))
9: {
10:return reader.ReadToEnd();
11: }
12: }).ContinueWith<ActionResult>(task =>
13: {
14:return Content((string)task.Result);
15: });
16: }
17: }
三、AsyncManager
在上⾯演⽰的异步Action的定义中,我们通过AsyncManager实现了两个基本的功能,即在异步操作和回调操作之间传递参数和向 MVC发送异步操作开始和结束的通知。

由于AsyncManager在异步Action场景中具有重要的作⽤,我们有必要对其进⾏单独介绍,下⾯是AsyncManager的定义。

1:public class AsyncManager
2: {
3:public AsyncManager();
4:public AsyncManager(SynchronizationContext syncContext);
5:
6:public EventHandler Finished;
7:
8:public virtual void Finish();
9:public virtual void Sync(Action action);
10:
11:public OperationCounter OutstandingOperations { get; }
12:public IDictionary<string, object> Parameters { get; }
13:public int Timeout { get; set; }
14: }
15:
16:public sealed class OperationCounter
17: {
18:public event EventHandler Completed;
19:
20:public int Increment();
21:public int Increment(int value);
22:public int Decrement();
23:public int Decrement(int value);
24:
25:public int Count { get; }
26: }
如上⾯的代码⽚断所⽰,AsyncManager具有两个构造函数重载,⾮默认构造函数接受⼀个表⽰同步上下⽂的SynchronizationContext对象作为参数。

如果指定的同步上下⽂对象为Null,并且当前的同步上下⽂(通过SynchronizationContext的静态属性Current表⽰)存在,则使⽤该上下⽂;否则创建⼀个新的同步上下⽂。

该同步上下⽂⽤于Sync⽅法的执⾏,也就是说在该⽅法指定的Action委托将会在该同步上下⽂中以同步的⽅式执⾏。

AsyncManager的核⼼是通过属性OutstandingOperations表⽰的正在进⾏的异步操作计数器,该属性是⼀个类型为的对象。

操作计数通过只读属性Count表⽰,当我们开始和完成异步操作的时候分别调⽤Increment和Decrement⽅法作增加和介绍计数操作。

Increment和Decrement各⾃具有两个重载,作为整数参数value(该参数值可以是负数)表⽰增加或者减少的数值,如果调⽤⽆参⽅法,增加或者减少的数值为1。

如果我们需要同时执⾏多个异步操作,则可以通过如下的⽅法来操作计数器。

1: AsyncManager.OutstandingOperations.Increment(3);
2:
3: Task.Factory.StartNew(() =>
4: {
5://异步操作1
6: AsyncManager.OutstandingOperations.Decrement();
7: });
8: Task.Factory.StartNew(() =>
9: {
10://异步操作2
11: AsyncManager.OutstandingOperations.Decrement();
12: });
13: Task.Factory.StartNew(() =>
14: {
15://异步操作3
16: AsyncManager.OutstandingOperations.Decrement();
17: });
对于每次通过Increment和Decrement⽅法调⽤引起的计数数值的改变,OperationCounter对象都会检验当前计数数值是否为零,如果则表明所有的操作运⾏完毕,如果预先注册了Completed事件,该事件会被触发。

值得⼀提的时候,表明所有操作完成执⾏的标志是计数器的值等于零,⽽不是⼩于零,如果我们通过调⽤Increment和Decrement ⽅法使计数器的值称为⼀个负数,注册的Completed事件是不会被触发的。

AsyncManager在初始化的时候就注册了通过属性OutstandingOperations表⽰的OperationCounter对象的Completed事件,使该事件触发的时候调⽤⾃⾝的Finish⽅法。

⽽虚⽅法Finish在AsyncManager中的默认实现⼜会触发⾃⾝的Finished事件。

如下⾯的代码⽚断所⽰,Controller类实现了接⼝,⽽后者定义了⼀个只读属性AsyncManager⽤于提供辅助执⾏异步Action的AsyncManager对象,⽽我们在定义异步Action ⽅法是使⽤的AsyncManager对象就是从抽象类Controller中集成下来的AsyncManager属性。

1:public abstract class Controller : ControllerBase, IAsyncManagerContainer,...
2: {
3:public AsyncManager AsyncManager { get; }
4: }
5:
6:public interface IAsyncManagerContainer
7: {
8: AsyncManager AsyncManager { get; }
9: }
四、Completed⽅法的执⾏
对于通过XxxAsync/XxxCompleted形式定义的异步Action,我们说回调操作XxxCompleted会在定义在XxxAsync⽅法中的异步操作执⾏结束之后被⾃动调⽤,那么XxxCompleted⽅法具体是如何被执⾏的呢?
异步Action的执⾏最终是通过描述该Action的AsyncActionDescriptor对象的BeginExecute/EndExecute⽅法来完成的。

通过之前“Model的绑定”的介绍我们知道通过XxxAsync/XxxCompleted形式定义的异步Action通过⼀个ReflectedAsyncActionDescriptor对象来表⽰的,ReflectedAsyncActionDescriptor在执⾏BeginExecute⽅法的时候会注册Controller对象的AsyncManager的Finished事件,使该事件触发的时候去执⾏Completed⽅法。

也就是说针对当前Controller的AsyncManager的Finished事件的触发标志着异步操作的结束,⽽此时匹配的Completed⽅法会被执⾏。

由于AsyncManager的Finish⽅法会主动触发该事件,所以我们可以通过调⽤该⽅法使Completed⽅法⽴即执⾏。

由于AsyncManager的OperationCounter对象的Completed事件触发的时候会调⽤Finish⽅法,所以当表⽰当前正在执⾏的异步操作计算器的值为零时,Completed⽅法也会⾃动被执⾏。

如果我们在XxxAsync⽅法中通过如下的⽅式同时执⾏三个异步操作,并在每个操作完成之后调⽤AsyncManager的Finish⽅法,意味着最先完成的异步操作会导致XxxCompleted⽅法的执⾏。

换句话说,当XxxCompleted⽅法执⾏的时候,可能还有两个异步操作正在执⾏。

1: AsyncManager.OutstandingOperations.Increment(3);
2:
3: Task.Factory.StartNew(() =>
4: {
5://异步操作1
6: AsyncManager.Finish();
7: });
8: Task.Factory.StartNew(() =>
9: {
10://异步操作2
11: AsyncManager.Finish();
12: });
13: Task.Factory.StartNew(() =>
14: {
15://异步操作3
16: AsyncManager.Finish();
17: });
如果完全通过为完成的异步操作计数机制来控制XxxCompleted⽅法的执⾏,由于计数的检测和Completed事件的触发只发⽣在OperationCounter的Increment/Decrement⽅法被执⾏的时候,如果我们在开始和结束异步操作的时候都没有调⽤这两个⽅法,XxxCompleted是否会执⾏呢?同样以之前定义的⽤语读取/显⽰⽂章内容的异步Action为例,我们按照如下的⽅式将定义在ArticleAsync⽅法中针对AsyncManager的OutstandingOperations属性的Increment和Decrement⽅法调⽤注释调⽤,ArticleCompleted⽅法是否还能正常运⾏呢?
1:public class HomeController : AsyncController
2: {
3:public void ArticleAsync(string name)
4: {
5://AsyncManager.OutstandingOperations.Increment();
6: Task.Factory.StartNew(() =>
7: {
8:string path = ControllerContext.HttpContext.Server.MapPath(string.Format(@"\articles\{0}.html", name));
9:using (StreamReader reader = new StreamReader(path))
10: {
11: AsyncManager.Parameters["content"] = reader.ReadToEnd();
12: }
13://AsyncManager.OutstandingOperations.Decrement();
14: });
15: }
16:public ActionResult ArticleCompleted(string content)
17: {
18:return Content(content);
19: }
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⽅法的定义是不对的。

1:public class HomeController : AsyncController
2: {
3:public void XxxAsync(string name)
4: {
5: Task.Factory.StartNew(() =>
6: {
7: AsyncManager.OutstandingOperations.Increment();
8://...
9: AsyncManager.OutstandingOperations.Decrement();
10: });
11: }
12://其他成员
13: }
下⾯采⽤正确的定义⽅法:
1:public class HomeController : AsyncController
2: {
3:public void XxxAsync(string name)
4: {
5: AsyncManager.OutstandingOperations.Increment();
6: Task.Factory.StartNew(() =>
7: {
8://...
9: AsyncManager.OutstandingOperations.Decrement();
10: });
11: }
12://其他成员
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异常发⽣。

1:public class HomeController : AsyncController
2: {
3:public Task<ActionResult> Data()
4: {
5:return Task.Factory.StartNew(() =>
6: {
7:while (true)
8: { }
9:return GetModel();
10:
11: }).ContinueWith<ActionResult>(task =>
12: {
13:object model = task.Result;
14:return View(task.Result);
15: });
16: }
17://其他成员
18: }
在 MVC应⽤编程接⼝中具有两个特殊的特性⽤于定制异步操作执⾏的超时时限,它们是具有如下定义的AsyncTimeoutAttribute和NoAsyncTimeoutAttribute,均定义在命名空间System.Web.Mvc下。

1: [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)]
2:public class AsyncTimeoutAttribute : ActionFilterAttribute
3: {
4:
5:public AsyncTimeoutAttribute(int duration);
6:public override void OnActionExecuting(ActionExecutingContext filterContext);
7:public int Duration { get; }
8: }
9:
10: [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)]
11:public sealed class NoAsyncTimeoutAttribute : AsyncTimeoutAttribute
12: {
13:// Methods
14:public NoAsyncTimeoutAttribute() : base(-1)
15: {
16: }
17: }
从上⾯给出的定义我们可以看出这两个特性均是ActionFilter。

AsyncTimeoutAttribute的构造函数接受⼀个表⽰超时时限(以毫秒为单位)的整数作为其参数,它通过重写OnActionExecuting⽅法将指定的超时时限设置给当前Controller的AsyncManager的Timeout属性进⾏。

NoAsyncTimeoutAttribute是AsyncTimeoutAttribute的继承者,它将超时时限设置为-1,意味着它解除了对超时的限制。

从应⽤在这两个特性的AttributeUsageAttribute定义可看出,它们既可以应⽤于类也可以⽤于也⽅法,意味着我们可以将它们应⽤到Controller类型或者异步Action⽅法(仅对XxxAsync⽅法有效,不能应⽤到XxxCompleted⽅法上)。

如果我们将它们同时应⽤到Controller类和Action⽅法上,针对⽅法级别的特性⽆疑具有更⾼的优先级。

相关文档
最新文档