ASP.NET MVC下的非同步Action的定義和執行原理[轉]

weixin_34067049發表於2015-02-03

http://www.cnblogs.com/artech/archive/2012/06/20/async-action-in-mvc.html

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的運算,採用非同步方法反而會因為執行緒排程和執行緒上下文的切換而影響整體的效能。

二、兩種非同步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屬性指定的引數值將自動作為對應的引數值。

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實現了兩個基本的功能,即在非同步操作和回撥操作之間傳遞引數和向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: }

 

四、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: }

在ASP.NET MVC應用程式設計介面中具有兩個特殊的特性用於定製非同步操作執行的超時時限,它們是具有如下定義的AsyncTimeoutAttributeNoAsyncTimeoutAttribute,均定義在名稱空間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方法上,針對方法級別的特性無疑具有更高的優先順序。

相關文章