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: 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屬性指定的參數值將自動作為對應的參數值。