这种同步的处理方式显然不合理,想要异步处理,只需按如下步骤进行:
1、替换基类Controller为AsyncController
2、创建两个配对的Action:ActionNameAsync和ActionNameCompleted。ActionNameAsync方法必须返回void,在内部启动一个耗时的IO操作前,需要使用AsyncManager.OutstandingOperations.Increment()向MVC框架“注册启动”,在IO方法返回后,可以在AsyncManager.Parameters字典中保存希望传给ActionNameCompleted方法的参数。最后调用AsyncManager.OutstandingOperations.Decrement()通知MVC框架操作完成,此时,MVC框架会自动调用ActionNameCompleted。ActionNameCompleted需要向通常的Action一样,返回一个ActionResult。因此上面的代码需要改写成如下这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public void GetPhotoByTagAsync( string tag) { //向MVC中注册启动 AsyncManager.OutstandingOperations.Increment(); ... WebRequest request = WebRequest.Create(url); //启动一个异步的web request request.BeginGetResponse(asyncResult => { using (WebResponse response = request.EndGetResponse(asyncResult)) { var xml = XDocument.Load(XmlReader.Create(response.GetResponseStream())); ... //将结果photoUrls,保存在AsyncManager.Parameters中 AsyncManager.Parameters[ "photoUrls" ] = photoUrls; //通知MVC框架操作完成 ,准备调用Completed AsyncManager.OutstandingOperations.Decrement(); } }, null ); } //像通常的Action一样,这里的参数photoUrls将在AsyncManager.Parameters中匹配 public ContentResult GetPhotoByTagCompleted(IEnumerable< string > photoUrls) { return Content( string .Format( "<img src='{0}'/>" , photoUrls.First())); } |
当然,可以设置异步操作的超时时间:
1 2 | [AsyncTimeout(10000)] // 10000 milliseconds equals 10 seconds public void GetPhotoByTagAsync( string tag) { ... } |
上面的代码如果超时了,将抛出TimeoutException异常,我们可以用希望的方式处理它。
当使用类似BeginGetResponse这类的异步方法,并提供回调函数参数时,你无法控制回调函数调用在哪个线程上。大多数情况下,甚至不在ASP.NET的工作线程上。所以回调函数无法关联原始的HttpContext对象。
幸好,AsyncManager提供了一个Sync()方法,它会将一个委托在ASP.NET的工作线程上启动,并关联原始的HttpContext对象。而且它保证线程安全:
1 2 3 4 5 6 7 8 9 10 11 12 | BeginAsyncOperation(asyncResult => { var result = EndAsyncOperation(asyncResult); // Can't always access System.Web.HttpContext.Current from here... Action doSomethingWithHttpContext = () => { // ... but can always access it from this delegate }; if (asyncResult.CompletedSynchronously) // Already on an ASP.NET thread doSomethingWithHttpContext(); else // Must switch to an ASP.NET thread AsyncManager.Sync(doSomethingWithHttpContext); AsyncManager.OutstandingOperations.Decrement(); }, null ); |
以上内容只是书中的内容摘录。对异步处理请求,我也没有深入研究,等以后用到了再回过来研究吧。
关于为什么使用异步Controller,这里不做备忘,三岁小孩都懂。主要的备忘是如何使用AsyncController。
此外还要另外注意几点:
1.对于异步请求,当发起另外一个线程去处理请求没有返回怎么办,比如抛出异常?框架默认的超时时间是45秒,在45秒到了之后框架会抛出一个System.TimeoutException以中止这个异步请求,我们可以通过[AsyncTimeOut((int duration)]来设置超时时间,还可以通过NoAsyncTimeout或者[AsyncTimeout(Timeout.Infinite)]来设置永不过期。
2.可以使用AsyncManager.Finish方法来中止所有还未结束的异步操作,进而调用Completed action,如果被强制中止的异步操作还没有成功返回某些参数时,Completed将使用这些参数的默认值(如int为0,string为empty)。
3.AsyncManager.Sync方法的作用
--------------------------------------------------------------------------------------------
可以通过 类编写异步操作方法。 可以对长时间运行的、非 CPU 绑定的请求使用异步操作方法。 这样可避免在处理请求时阻塞 Web 服务器执行工作。 类通常用于长时间运行的 Web 服务调用。
本主题包含以下各节:
与本主题对应的包含源代码的 Visual Studio 项目可从 (下载)网页获得。
在 Web 服务器上,.NET Framework 维护一个用于服务 ASP.NET 请求的线程池。 当请求到达时,将调度池中的线程以处理该请求。 如果对请求进行同步处理,则在处理请求时将阻塞处理请求的线程,并且该线程不能对另一个请求提供服务。
这可能不是一个问题,因为线程池可以设置得足够大以容纳许多阻塞的线程。 但是,线程池中的线程数目是有限制的。 在同时处理多个长时间运行的请求的大型应用程序中,可能会阻塞所有可用的线程。 这种情况称为“线程不足”。 当出现这种情况时,Web 服务器会将请求排队。 如果请求队列已满,则 Web 服务器会拒绝请求并处于 HTTP 503 状态(服务器太忙)。
在可能出现线程不足的应用程序中,您可以配置通过异步方式处理操作。 异步请求与同步请求所需的处理时间相同。 例如,如果某个请求生成一个需要两秒钟来完成的网络调用,则该请求无论是同步执行还是异步执行都需要两秒钟。 但是,在异步调用的过程中,服务器在等待第一个请求完成的过程中不会阻塞对其他请求的响应。 因此,当有许多请求调用长时间运行的操作时,异步请求可以防止出现请求排队的情况。
在调用异步操作时,将执行以下步骤:
-
Web 服务器从线程池(辅助线程)获取一个线程并安排它处理传入请求。 此辅助线程启动一个异步操作。
-
将此辅助线程返回到线程池以对另一个 Web 请求提供服务。
-
在异步操作完成时通知 ASP.NET。
-
Web 服务器从线程池获取一个线程(可能是与启动异步操作的线程不同的线程)以处理请求的其余部分,包括呈现响应。
下图显示了异步模式。
本节列出了有关何时使用同步操作方法或异步操作方法的准则。 这只是一些准则;您必须逐个检查每个应用程序以确定异步操作方法是否能帮助提高性能。
通常,在满足以下条件时使用同步管线:
-
操作很简单或运行时间很短。
-
简单性比效率更重要。
-
此操作主要是 CPU 操作而不是包含大量的磁盘或网络开销的操作。 对 CPU 绑定操作使用异步操作方法未提供任何好处并且还导致更多的开销。
通常,在满足以下条件时使用异步管线:
-
操作是网络绑定的或 I/O 绑定的而不是 CPU 绑定的。
-
测试显示阻塞操作对于网站性能是一个瓶颈,并且通过对这些阻塞调用使用异步操作方法,IIS 可对更多的请求提供服务。
-
并行性比代码的简单性更重要。
-
您希望提供一种可让用户取消长时间运行的请求的机制。
下载的示例演示如何有效地使用异步操作方法。 示例程序调用 方法来模拟长时间运行的进程。 很少有产品应用程序会显示出如此明显的使用异步操作方法的好处。
您应测试应用程序以确定异步方法是否能提供性能好处。 在某些情况下,增加每个 CPU 的 IIS 最大并发请求数和每个 CPU 的最大并发线程数可能会更好。 有关 ASP.NET 线程配置的更多信息,请参见 Thomas Marquardt 的博客上的文章 (ASP.NET 线程在 IIS 7.0 和 6.0 上的使用情况)。 有关何时执行异步数据库调用的更多信息,请参见 Rick Anderson 博客上的文章 (我的数据库调用是否应采用异步方式?)。
很少有应用程序要求所有的操作方法都是异步的。 通常,将少量的同步操作方法转换为异步方法就会显著增加所需的工作量。
下面的代码示例演示了一个同步操作方法,它用于显示来自门户网站控制器的新闻项。 请求 Portal/News?city=Seattle 显示 Seattle 的新闻。
public class PortalController: Controller { public ActionResult News(string city) { NewsService newsService = new NewsService(); ViewStringModel headlines = newsService.GetHeadlines(city); return View(headlines); }}
下面的示例演示了重新编写为异步方法的 News 操作方法。
public class PortalController : AsyncController { public void NewsAsync(string city) { AsyncManager.OutstandingOperations.Increment(); NewsService newsService = new NewsService(); newsService.GetHeadlinesCompleted += (sender, e) => { AsyncManager.Parameters["headlines"] = e.Value; AsyncManager.OutstandingOperations.Decrement(); }; newsService.GetHeadlinesAsync(city); } public ActionResult NewsCompleted(string[] headlines) { return View("News", new ViewStringModel { NewsHeadlines = headlines }); }}
将同步操作方法转换为异步操作方法包含以下步骤:
-
不要从 派生控制器,而应从 派生。 从 派生的控制器使 ASP.NET 能够处理异步请求,并且这些控制器仍然可以为同步操作方法提供服务。
-
为操作创建两个方法。 启动异步进程的方法必须具有一个由操作和后缀“Async”组成的名称。 异步进程完成(回调方法)时调用的方法必须具有一个由操作和后缀“Completed”组成的名称。 在前面的示例中,News 方法已转换为两个方法:NewsAsync 和 NewsCompleted。
NewsAsync 方法返回 void(在 Visual Basic 中没有任何值)。 NewsCompleted 方法返回 实例。 尽管操作由两个方法组成,但使用与同步操作方法相同的 URL 来访问它(例如 Portal/News?city=Seattle)。 其他方法(例如 和 )还是将按照 News 而不是 NewsAsync 来引用操作方法。
传递到 NewsAsync 的参数使用普通的参数绑定机制。 传递到 NewsCompleted 的参数使用 字典。
-
使用异步操作方法中的异步调用替换原始 方法中的同步调用。 在上面的示例中,使用对 newsService.GetHeadlinesAsync 的调用替换对newsService.GetHeadlines 的调用。
由 NewsAsync 方法使用的 NewsService 类是一个使用基于事件的异步模式公开方法的服务示例。 有关此模式的更多信息,请参见。
属性通知 ASP.NET 有多少个操作已挂起。 这是必要的,因为 ASP.NET 不能确定由操作方法启动了多少个操作或这些操作何时完成。 当 属性为零时,ASP.NET 可通过调用 NewsCompleted 方法来完成整个异步操作。
请注意下面有关异步操作方法的一些事项:
-
如果操作名称为 Sample,则框架将查找 SampleAsync 和 SampleCompleted 方法。
-
视图页应命名为 Sample.aspx,而不是命名为 SampleAsync.aspx 或 SampleCompleted.aspx。 (操作名称为 Sample,而不是为 SampleAsync。)
-
控制器不能包含名为 SampleAsync 的异步方法和名为 Sample 的同步方法。 如果包含这两个方法,则会引发 异常,因为SampleAsync 操作方法和 Sample 操作方法具有相同的请求签名。
当操作必须执行几个独立的操作时,异步操作方法很有用。 例如,门户网站可能不只显示新闻,还显示体育、天气、股票和其他信息。
下面的示例演示了新闻门户网站 Index 操作方法的同步版本。
public ActionResult IndexSynchronous( string city ) { NewsService newsService = new NewsService(); string[] headlines = newsService.GetHeadlines(); SportsService sportsService = new SportsService(); string[] scores = sportsService.GetScores(); WeatherService weatherService = new WeatherService(); string[] forecast = weatherService.GetForecast(); return View("Common", new PortalViewModel { NewsHeadlines = headlines, SportsScores = scores, Weather = forecast });}
按顺序执行对每个服务的调用。 因此,为了响应请求所需的时间是每个服务调用的时间加上少量系统开销的时间的总和。 例如,如果各个调用分别用了 400、500 和 600 毫秒,则总的响应时间将稍微大于 1.5 秒。 但是,如果异步执行服务调用(以并行方式),则总的响应时间将稍微大于 600 毫秒,因为这是最长任务的持续时间。
下面的示例演示了新闻门户网站 Index 操作方法的异步版本。
public void IndexAsync(string city) { AsyncManager.OutstandingOperations.Increment(3); NewsService newsService = new NewsService(); newsService.GetHeadlinesCompleted += (sender, e) => { AsyncManager.Parameters["headlines"] = e.Value; AsyncManager.OutstandingOperations.Decrement(); }; newsService.GetHeadlinesAsync(); SportsService sportsService = new SportsService(); sportsService.GetScoresCompleted += (sender, e) => { AsyncManager.Parameters["scores"] = e.Value; AsyncManager.OutstandingOperations.Decrement(); }; sportsService.GetScoresAsync(); WeatherService weatherService = new WeatherService(); weatherService.GetForecastCompleted += (sender, e) => { AsyncManager.Parameters["forecast"] = e.Value; AsyncManager.OutstandingOperations.Decrement(); }; weatherService.GetForecastAsync();}public ActionResult IndexCompleted(string[] headlines, string[] scores, string[] forecast) { return View("Common", new PortalViewModel { NewsHeadlines = headlines, SportsScores = scores, Weather = forecast });} }
在前面的示例中,使用参数 3 调用 方法,这是因为有三个异步操作。
如果要将特性应用于异步操作方法,则将它们应用于 ActionAsync 方法,而不是应用于 ActionCompleted 方法。 忽略 ActionCompleted 方法上的特性。
已添加两个新的特性: 和 。 这些特性可让您控制异步超时时间。
如果异步操作方法调用一个使用 BeginMethod/EndMethod 模式公开方法的服务,则回调方法(即作为异步回调参数传递到 Begin 方法的方法)可能会在一个不由 ASP.NET 控制的线程上执行。 在此情况下,HttpContext.Current 将为 null,并且当应用程序访问 类的成员(例如 )时可能会出现争用条件。 若要确保已访问 HttpContext.Current 实例并避免争用条件,则可以通过从回调方法中调用 Sync 来还原 HttpContext.Current。
如果回调同步完成,则回调将在由 ASP.NET 控制的线程上执行并且将对操作进行序列化,因此不会出现并发问题。 从已经由 ASP.NET 控制的线程中调用 Sync 具有未定义的行为。
将总是在由 ASP.NET 控制的线程上调用 ActionCompleted 方法。 因此,不要从该方法中调用 Sync。
传递到 Begin 方法的回调可能会使用由 ASP.NET 控制的线程来进行调用。 因此,您必须在调用 Sync 之前检查此条件。 如果操作已同步完成(即,如果 为 true),则回调在原始线程上执行,并且您不必调用 Sync。 如果操作已异步完成,(即, 为 false),则回调在线程池或 I/O 完成端口线程上执行,并且您必须 Sync。
有关 BeginMethod/EndMethod 模式的更多信息,请参见和 Rick Anderson 的博客上的文章 (对 MVC 使用 BeginMethod/EndMethod 模式)。
下表列出了异步操作方法的关键类。
类 | 描述 |
---|---|
为异步控制器提供基类。 | |
为 类提供异步操作。 |