ASP.NET MVC下的異步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的運算,采用異步方法反而會因為線程調度和線程上下文的切換而影響整體的性能。
在了解了在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方法來說,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: 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在執行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方法被執行的時候,同樣以之前定義的用語讀取/顯示文章內容的異步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方法會在ArticleAsync方法執行之後被立即執行。如果文章內容讀取是一個相對耗時的操作,表示文章內容的ArticleCompleted方法的content參數在執行的時候尚未被初始化。在這種情況下的ArticleCompleted是如何被執行的呢?
原因和簡單,。對於我們給出的例子來說,在執行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屬性設置為,意味著異步操作執行不再具有任何時間的限製。對於以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應用編程接口中具有兩個特殊的特性用於定製異步操作執行的超時時限,它們是具有如下定義的和,均定義在命名空間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的繼承者,它將超時時限設置為,意味著它解除了對超時的限製。
從應用在這兩個特性的AttributeUsageAttribute定義可看出,它們既可以應用於類也可以用於也方法,意味著我們可以將它們應用到Controller類型或者異步Action方法(僅對XxxAsync方法有效,不能應用到XxxCompleted方法上)。如果我們將它們同時應用到Controller類和Action方法上,針對方法級別的特性無疑具有更高的優先級。
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
最後更新:2017-10-26 11:04:39