http://www.cnblogs.com/artech/archive/2012/06/20/async-action-in-mvc.htmlhtml
Visual Studio提供的Controller建立嚮導默認爲咱們建立一個繼承自抽象類Controller的Controller類型,這樣的Controller只能定義同步Action方法。若是咱們須要定義異步Action方法,必須繼承抽象類AsyncController。這篇問你講述兩種不一樣的異步Action的定義方法和底層執行原理。[本文已經同步到《How ASP.NET MVC Works?》中]數據庫
目錄
1、基於線程池的請求處理
2、兩種異步Action方法的定義
XxxAsync/XxxCompleted
Task返回值
3、AsyncManager
4、Completed方法的執行
5、異步操做的超時控制編程
ASP.NET經過線程池的機制處理併發的HTTP請求。一個Web應用內部維護着一個線程池,當探測到抵達的針對本應用的請求時,會從池中獲取一個空閒的線程來處理該請求。當處理完畢,線程不會被回收,而是從新釋放到池中。線程池具備一個線程的最大容量,若是建立的線程達到這個上限而且全部的線程均被處於「忙碌」狀態,新的HTTP請求會被放入一個請求隊列以等待某個完成了請求處理任務的線程從新釋放到池中。服務器
咱們將這些用於處理HTTP請求的線程稱爲工做線程(Worker Thread),而這個縣城池天然就叫作工做線程池。ASP.NET這種基於線程池的請求處理機制主要具備以下兩個優點:併發
若是請求處理操做耗時較短,那麼工做線程處理完畢後能夠及時地被釋放到線程池中以用於對下一個請求的處理。可是對於比較耗時的操做來講,意味着工做線程將被長時間被某個請求獨佔,若是這樣的操做訪問比較頻繁,在高併發的狀況下意味着線程池中將可能找不到空閒的工做線程用於及時處理最新抵達請求。mvc
若是咱們採用異步的方式來處理這樣的耗時請求,工做線程可讓後臺線程來接手,本身能夠及時地被釋放到線程池中用於進行後續請求的處理,從而提升了整個服務器的吞吐能力。值得一提的是,異步操做主要用於I/O綁定操做(好比數據庫訪問和遠程服務調用等),而非CPU綁定操做,由於異步操做對總體性能的提高來源於:當I/O設備在處理某個任務的時候,CPU能夠釋放出來處理另外一個任務。若是耗時操做主要依賴於本機CPU的運算,採用異步方法反而會由於線程調度和線程上下文的切換而影響總體的性能。app
在瞭解了在AsyncController中定義異步Action方法的必要性以後,咱們來簡單介紹一下異步Action方法的定義方式。總的來講,異步Action方法具備兩種定義方式,一種是將其定義成兩個匹配的方法XxxAsync/XxxCompleted,另外一種則是定義一個返回類型爲Task的方法。異步
若是咱們使用兩個匹配的方法XxxAsync/XxxCompleted來定義異步Action,咱們能夠將異步操做實如今XxxAsync方法中,而將最終內容的呈現實如今XxxCompleted方法中。XxxCompleted能夠當作是針對XxxAsync的回調,當定義在XxxAsync方法中的操做以異步方式執行完成後,XxxCompleted方法會被自動調用。XxxCompleted的定義方式和普通的同步Action方法比較相似。async
做爲演示,我在以下一個HomeController中定義了一個名爲Article的異步操做來呈現指定名稱的文章內容。咱們將指定文章內容的異步讀取定義在ArticleAsync方法中,而在ArticleCompleted方法中講讀取的內容以ContentResult的形式呈現出來。ide
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方法來講,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屬性指定的參數值將自動做爲對應的參數值。
若是採用上面的異步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: }
在上面演示的異步Action的定義中,咱們經過AsyncManager實現了兩個基本的功能,即在異步操做和回調操做之間傳遞參數和向ASP.NET 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表示的正在進行的異步操做計數器,該屬性是一個類型爲OperationCounter的對象。操做計數經過只讀屬性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類實現了IAsyncManagerContainer接口,然後者定義了一個只讀屬性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: }
對於經過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: }
在ASP.NET 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方法上,針對方法級別的特性無疑具備更高的優先級。