詳解ASP.NET MVC下的異步Action的定義和執(zhí)行原理
Visual Studio提供的Controller創(chuàng)建向?qū)J(rèn)為我們創(chuàng)建一個繼承自抽象類Controller的Controller類型,這樣的Controller只能定義同步Action方法。如果我們需要定義異步Action方法,必須繼承抽象類AsyncController。這篇問你講述兩種不同的異步Action的定義方法和底層執(zhí)行原理。
一、基于線程池的請求處理
ASP.NET通過線程池的機制處理并發(fā)的HTTP請求。一個Web應(yīng)用內(nèi)部維護著一個線程池,當(dāng)探測到抵達(dá)的針對本應(yīng)用的請求時,會從池中獲取一個空閑的線程來處理該請求。當(dāng)處理完畢,線程不會被回收,而是重新釋放到池中。線程池具有一個線程的最大容量,如果創(chuàng)建的線程達(dá)到這個上限并且所有的線程均被處于“忙碌”狀態(tài),新的HTTP請求會被放入一個請求隊列以等待某個完成了請求處理任務(wù)的線程重新釋放到池中。
我們將這些用于處理HTTP請求的線程稱為工作線程(Worker Thread),而這個縣城池自然就叫做工作線程池。ASP.NET這種基于線程池的請求處理機制主要具有如下兩個優(yōu)勢:
- 工作線程的重用:創(chuàng)建線程的成本雖然不如進程的激活,卻也不是一件“一蹴而就”的事情,頻繁地創(chuàng)建和釋放線程會對性能造成極大的損害。而線程池機制避免了總是創(chuàng)建新的工作線程來處理每一個請求,被創(chuàng)建的工作線程得到了極大地重用,并最終提高了服務(wù)器的吞吐能力。
- 工作線程數(shù)量的限制:資源的有限性具有了服務(wù)器處理請求的能力具有一個上限,或者說某臺服務(wù)器能夠處理的請求并發(fā)量具有一個臨界點,一旦超過這個臨界點,整臺服務(wù)將會因不能提供足夠的資源而崩潰。由于采用了對工作線程數(shù)量具有良好控制的線程池機制,ASP.NET MVC并發(fā)處理的請求數(shù)量不可能超過線程池的最大允許的容量,從而避免了在高并發(fā)情況下工作線程的無限制創(chuàng)建而最導(dǎo)致整個服務(wù)器的崩潰。
如果請求處理操作耗時較短,那么工作線程處理完畢后可以及時地被釋放到線程池中以用于對下一個請求的處理。但是對于比較耗時的操作來說,意味著工作線程將被長時間被某個請求獨占,如果這樣的操作訪問比較頻繁,在高并發(fā)的情況下意味著線程池中將可能找不到空閑的工作線程用于及時處理最新抵達(dá)請求。
如果我們采用異步的方式來處理這樣的耗時請求,工作線程可以讓后臺線程來接手,自己可以及時地被釋放到線程池中用于進行后續(xù)請求的處理,從而提高了整個服務(wù)器的吞吐能力。值得一提的是,異步操作主要用于I/O綁定操作(比如數(shù)據(jù)庫訪問和遠(yuǎn)程服務(wù)調(diào)用等),而非CPU綁定操作,因為異步操作對整體性能的提升來源于:當(dāng)I/O設(shè)備在處理某個任務(wù)的時候,CPU可以釋放出來處理另一個任務(wù)。如果耗時操作主要依賴于本機CPU的運算,采用異步方法反而會因為線程調(diào)度和線程上下文的切換而影響整體的性能。
二、兩種異步Action方法的定義
在了解了在AsyncController中定義異步Action方法的必要性之后,我們來簡單介紹一下異步Action方法的定義方式。總的來說,異步Action方法具有兩種定義方式,一種是將其定義成兩個匹配的方法XxxAsync/XxxCompleted,另一種則是定義一個返回類型為Task的方法。
XxxAsync/XxxCompleted
如果我們使用兩個匹配的方法XxxAsync/XxxCompleted來定義異步Action,我們可以將異步操作實現(xiàn)在XxxAsync方法中,而將最終內(nèi)容的呈現(xiàn)實現(xiàn)在XxxCompleted方法中。XxxCompleted可以看成是針對XxxAsync的回調(diào),當(dāng)定義在XxxAsync方法中的操作以異步方式執(zhí)行完成后,XxxCompleted方法會被自動調(diào)用。XxxCompleted的定義方式和普通的同步Action方法比較類似。
作為演示,我在如下一個HomeController中定義了一個名為Article的異步操作來呈現(xiàn)指定名稱的文章內(nèi)容。我們將指定文章內(nèi)容的異步讀取定義在ArticleAsync方法中,而在ArticleCompleted方法中講讀取的內(nèi)容以ContentResult的形式呈現(xiàn)出來。
public class HomeController : AsyncController { public void ArticleAsync(string name) { AsyncManager.OutstandingOperations.Increment(); Task.Factory.StartNew(() => { string path = ControllerContext.HttpContext.Server.MapPath(string.Format(@"\articles\{0}.html", name)); using (StreamReader reader = new StreamReader(path)) { AsyncManager.Parameters["content"] = reader.ReadToEnd(); } AsyncManager.OutstandingOperations.Decrement(); }); } public ActionResult ArticleCompleted(string content) { return Content(content); } }
對于以XxxAsync/XxxCompleted形式定義的異步Action方法來說,ASP.NET MVC并不會以異步的方式來調(diào)用XxxAsync方法,所以我們需要在該方法中自定義實現(xiàn)異步操作的執(zhí)行。在上面定義的ArticleAsync方法中,我們是通過基于Task的并行編程方式來實現(xiàn)對文章內(nèi)容的異步讀取的。當(dāng)我們以XxxAsync/XxxCompleted形式定義的異步Action方法的時候,會頻繁地使用到Controller的AsyncManager屬性,該屬性返回一個類型為AsyncManager對象,我們將在下面一節(jié)對其進行單獨講述。
在上面提供的實例中,我們在異步操作開始和結(jié)束的時候調(diào)用了AsyncManager的OutstandingOperations屬性的Increment和Decrement方法對于ASP.NET MVC發(fā)起通知。此外,我們還利用AsyncManager的Parameters屬性表示的字典來保存?zhèn)鬟f給ArticleCompleted方法的參數(shù),參數(shù)在字典中的Key(content)與ArticleCompleted的參數(shù)名稱是匹配的,所以在調(diào)用方法ArticleCompleted的時候,通過AsyncManager的Parameters屬性指定的參數(shù)值將自動作為對應(yīng)的參數(shù)值。
Task返回值
如果采用上面的異步Action定義方式,意味著我們不得不為一個Action定義兩個方法,實際上我們可以通過一個方法來完成對異步Action的定義,那就是讓Action方法返回一個代表異步操作的Task對象。上面通過XxxAsync/XxxCompleted形式定義的異步Action可以采用如下的定義方式。
public class HomeController AsyncController { public Task<ActionResult> Article(string name) { return Task.Factory.StartNew(() => { string path = ControllerContext.HttpContext.Server.MapPath(string.Format(@"\articles\{0}.html", name)); using (StreamReader reader = new StreamReader(path)) { AsyncManager.Parameters["content"] = reader.ReadToEnd(); } }).ContinueWith<ActionResult>(task => { string content = (string)AsyncManager.Parameters["content"]; return Content(content); }); } }
上面定義的異步Action方法Article的返回類型為Task<ActionResult>,我們將異步文件內(nèi)容的讀取體現(xiàn)在返回的Task對象中。對文件內(nèi)容呈現(xiàn)的回調(diào)操作則通過調(diào)用該Task對象的ContinueWith<ActionResult>方法進行注冊,該操作會在異步操作完成之后被自動調(diào)用。
如上面的代碼片斷所示,我們依然利用AsyncManager的Parameters屬性實現(xiàn)參數(shù)在異步操作和回調(diào)操作之間的傳遞。其實我們也可以使用Task對象的Result屬性來實現(xiàn)相同的功能,Article方法的定義也改寫成如下的形式。
public class HomeController AsyncController { public Task<ActionResult> Article(string name) { return Task.Factory.StartNew(() => { string path = ControllerContext.HttpContext.Server.MapPath(string.Format(@"\articles\{0}.html", name)); using (StreamReader reader = new StreamReader(path)) { return reader.ReadToEnd(); } }).ContinueWith<ActionResult>(task => { return Content((string)task.Result); }); } }
三、AsyncManager
在上面演示的異步Action的定義中,我們通過AsyncManager實現(xiàn)了兩個基本的功能,即在異步操作和回調(diào)操作之間傳遞參數(shù)和向ASP.NET MVC發(fā)送異步操作開始和結(jié)束的通知。由于AsyncManager在異步Action場景中具有重要的作用,我們有必要對其進行單獨介紹,下面是AsyncManager的定義。
public class AsyncManager { public AsyncManager(); public AsyncManager(SynchronizationContext syncContext); public EventHandler Finished; public virtual void Finish(); public virtual void Sync(Action action); public OperationCounter OutstandingOperations { get; } public IDictionary<string, object> Parameters { get; } public int Timeout { get; set; } } public sealed class OperationCounter { public event EventHandler Completed; public int Increment(); public int Increment(int value); public int Decrement(); public int Decrement(int value); public int Count { get; } }
如上面的代碼片斷所示,AsyncManager具有兩個構(gòu)造函數(shù)重載,非默認(rèn)構(gòu)造函數(shù)接受一個表示同步上下文的SynchronizationContext對象作為參數(shù)。如果指定的同步上下文對象為Null,并且當(dāng)前的同步上下文(通過SynchronizationContext的靜態(tài)屬性Current表示)存在,則使用該上下文;否則創(chuàng)建一個新的同步上下文。該同步上下文用于Sync方法的執(zhí)行,也就是說在該方法指定的Action委托將會在該同步上下文中以同步的方式執(zhí)行。
AsyncManager的核心是通過屬性O(shè)utstandingOperations表示的正在進行的異步操作計數(shù)器,該屬性是一個類型為OperationCounter的對象。操作計數(shù)通過只讀屬性Count表示,當(dāng)我們開始和完成異步操作的時候分別調(diào)用Increment和Decrement方法作增加和介紹計數(shù)操作。Increment和Decrement各自具有兩個重載,作為整數(shù)參數(shù)value(該參數(shù)值可以是負(fù)數(shù))表示增加或者減少的數(shù)值,如果調(diào)用無參方法,增加或者減少的數(shù)值為1。如果我們需要同時執(zhí)行多個異步操作,則可以通過如下的方法來操作計數(shù)器。
AsyncManager.OutstandingOperations.Increment(3); Task.Factory.StartNew(() => { //異步操作1 AsyncManager.OutstandingOperations.Decrement(); }); Task.Factory.StartNew(() => { //異步操作2 AsyncManager.OutstandingOperations.Decrement(); }); Task.Factory.StartNew(() => { //異步操作3 AsyncManager.OutstandingOperations.Decrement(); });
對于每次通過Increment和Decrement方法調(diào)用引起的計數(shù)數(shù)值的改變,OperationCounter對象都會檢驗當(dāng)前計數(shù)數(shù)值是否為零,如果則表明所有的操作運行完畢,如果預(yù)先注冊了Completed事件,該事件會被觸發(fā)。值得一提的時候,表明所有操作完成執(zhí)行的標(biāo)志是計數(shù)器的值等于零,而不是小于零,如果我們通過調(diào)用Increment和Decrement方法使計數(shù)器的值稱為一個負(fù)數(shù),注冊的Completed事件是不會被觸發(fā)的。
AsyncManager在初始化的時候就注冊了通過屬性O(shè)utstandingOperations表示的OperationCounter對象的Completed事件,使該事件觸發(fā)的時候調(diào)用自身的Finish方法。而虛方法Finish在AsyncManager中的默認(rèn)實現(xiàn)又會觸發(fā)自身的Finished事件。
如下面的代碼片斷所示,Controller類實現(xiàn)了IAsyncManagerContainer接口,而后者定義了一個只讀屬性AsyncManager用于提供輔助執(zhí)行異步Action的AsyncManager對象,而我們在定義異步Action方法是使用的AsyncManager對象就是從抽象類Controller中集成下來的AsyncManager屬性。
public abstract class Controller ControllerBase, IAsyncManagerContainer,... { public AsyncManager AsyncManager { get; } } public interface IAsyncManagerContainer { AsyncManager AsyncManager { get; } }
四、Completed方法的執(zhí)行
對于通過XxxAsync/XxxCompleted形式定義的異步Action,我們說回調(diào)操作XxxCompleted會在定義在XxxAsync方法中的異步操作執(zhí)行結(jié)束之后被自動調(diào)用,那么XxxCompleted方法具體是如何被執(zhí)行的呢?
異步Action的執(zhí)行最終是通過描述該Action的AsyncActionDescriptor對象的BeginExecute/EndExecute方法來完成的。通過之前“Model的綁定”的介紹我們知道通過XxxAsync/XxxCompleted形式定義的異步Action通過一個ReflectedAsyncActionDescriptor對象來表示的,ReflectedAsyncActionDescriptor在執(zhí)行BeginExecute方法的時候會注冊Controller對象的AsyncManager的Finished事件,使該事件觸發(fā)的時候去執(zhí)行Completed方法。
也就是說針對當(dāng)前Controller的AsyncManager的Finished事件的觸發(fā)標(biāo)志著異步操作的結(jié)束,而此時匹配的Completed方法會被執(zhí)行。由于AsyncManager的Finish方法會主動觸發(fā)該事件,所以我們可以通過調(diào)用該方法使Completed方法立即執(zhí)行。由于AsyncManager的OperationCounter對象的Completed事件觸發(fā)的時候會調(diào)用Finish方法,所以當(dāng)表示當(dāng)前正在執(zhí)行的異步操作計算器的值為零時,Completed方法也會自動被執(zhí)行。
如果我們在XxxAsync方法中通過如下的方式同時執(zhí)行三個異步操作,并在每個操作完成之后調(diào)用AsyncManager的Finish方法,意味著最先完成的異步操作會導(dǎo)致XxxCompleted方法的執(zhí)行。換句話說,當(dāng)XxxCompleted方法執(zhí)行的時候,可能還有兩個異步操作正在執(zhí)行。
AsyncManager.OutstandingOperations.Increment(3); Task.Factory.StartNew(() => { //異步操作1 AsyncManager.Finish(); }); Task.Factory.StartNew(() => { //異步操作2 AsyncManager.Finish(); }); Task.Factory.StartNew(() => { //異步操作3 AsyncManager.Finish(); });
如果完全通過為完成的異步操作計數(shù)機制來控制XxxCompleted方法的執(zhí)行,由于計數(shù)的檢測和Completed事件的觸發(fā)只發(fā)生在OperationCounter的Increment/Decrement方法被執(zhí)行的時候,如果我們在開始和結(jié)束異步操作的時候都沒有調(diào)用這兩個方法,XxxCompleted是否會執(zhí)行呢?同樣以之前定義的用語讀取/顯示文章內(nèi)容的異步Action為例,我們按照如下的方式將定義在ArticleAsync方法中針對AsyncManager的OutstandingOperations屬性的Increment和Decrement方法調(diào)用注釋調(diào)用,ArticleCompleted方法是否還能正常運行呢?
public class HomeController AsyncController { public void ArticleAsync(string name) { //AsyncManager.OutstandingOperations.Increment(); Task.Factory.StartNew(() => { string path = ControllerContext.HttpContext.Server.MapPath(string.Format(@"\articles\{0}.html", name)); using (StreamReader reader = new StreamReader(path)) { AsyncManager.Parameters["content"] = reader.ReadToEnd(); } //AsyncManager.OutstandingOperations.Decrement(); }); } public ActionResult ArticleCompleted(string content) { return Content(content); } }
實際上ArticleCompleted依然會被執(zhí)行,但是這樣我們就不能確保正常讀取文章內(nèi)容,因為ArticleCompleted方法會在ArticleAsync方法執(zhí)行之后被立即執(zhí)行。如果文章內(nèi)容讀取是一個相對耗時的操作,表示文章內(nèi)容的ArticleCompleted方法的content參數(shù)在執(zhí)行的時候尚未被初始化。在這種情況下的ArticleCompleted是如何被執(zhí)行的呢?
原因和簡單,ReflectedAsyncActionDescriptor的BeginExecute方法在執(zhí)行XxxAsync方法的前后會分別調(diào)用AsyncManager的OutstandingOperations屬性的Increment和Decrement方法。對于我們給出的例子來說,在執(zhí)行ArticleAsync之前Increment方法被調(diào)用使計算器的值變成1,隨后ArticleAsync被執(zhí)行,由于該方法以異步的方式讀取指定的文件內(nèi)容,所以會立即返回。最后Decrement方法被執(zhí)行使計數(shù)器的值變成0,AsyncManager的Completed事件被觸發(fā)并導(dǎo)致ArticleCompleted方法的執(zhí)行。而此時,文件內(nèi)容的讀取正在進行之中,表示文章內(nèi)容的content參數(shù)自然尚未被初始化。
ReflectedAsyncActionDescriptor這樣的執(zhí)行機制也對我們使用AsyncManager提出了要求,那就是對尚未完成的一步操作計數(shù)器的增加操作不應(yīng)該發(fā)生在異步線程中,如下所示的針對AsyncManager的OutstandingOperations屬性的Increment方法的定義是不對的。
public class HomeController AsyncController { public void XxxAsync(string name) { Task.Factory.StartNew(() => { AsyncManager.OutstandingOperations.Increment(); //... AsyncManager.OutstandingOperations.Decrement(); }); } //其他成員 }
下面采用正確的定義方法:
public class HomeController AsyncController { public void XxxAsync(string name) { AsyncManager.OutstandingOperations.Increment(); Task.Factory.StartNew(() => { //... AsyncManager.OutstandingOperations.Decrement(); }); } //其他成員 }
最后再強調(diào)一點,不論是顯式調(diào)用AsyncManager的Finish方法,還是通過調(diào)用AsyncManager的OutstandingOperations屬性的Increment方法是計數(shù)器的值變成零,僅僅是讓XxxCompleted方法得以執(zhí)行,并不能真正阻止異步操作的執(zhí)行。
五、異步操作的超時控制
異步操作雖然適合那些相對耗時的I/O綁定型操作,但是也并不說對一步操作執(zhí)行的時間沒有限制。異步超時時限通過AsyncManager的整型屬性Timeout表示,它表示超時時限的總毫秒數(shù),其默認(rèn)值為45000(45秒)。如果將Timeout屬性設(shè)置為-1,意味著異步操作執(zhí)行不再具有任何時間的限制。對于以XxxAsync/XxxCompleted形式定義的異步Action來說,如果XxxAsync執(zhí)行之后,在規(guī)定的超時時限中XxxCompleted沒有得到執(zhí)行,一個TimeoutException會被拋出來。
如果我們以返回類型為Task的形式定義異步Action,通過Task體現(xiàn)的異步操作的執(zhí)行時間不受AsyncManager的Timeout屬性的限制。我們通過如下的代碼定義了一個名為Data的異步Action方法以異步的方式獲取作為Model的數(shù)據(jù)并通過默認(rèn)的View呈現(xiàn)出來,但是異步操作中具有一個無限循環(huán),當(dāng)我們訪問該Data方法時,異步操作將會無限制地執(zhí)行下去,也不會有TimeoutException異常發(fā)生。
public class HomeController AsyncController { public Task<ActionResult> Data() { return Task.Factory.StartNew(() => { while (true) { } return GetModel(); }).ContinueWith<ActionResult>(task => { object model = task.Result; return View(task.Result); }); } //其他成員 }
在ASP.NET MVC應(yīng)用編程接口中具有兩個特殊的特性用于定制異步操作執(zhí)行的超時時限,它們是具有如下定義的AsyncTimeoutAttribute和NoAsyncTimeoutAttribute,均定義在命名空間System.Web.Mvc下。
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)] public class AsyncTimeoutAttribute ActionFilterAttribute { public AsyncTimeoutAttribute(int duration); public override void OnActionExecuting(ActionExecutingContext filterContext); public int Duration { get; } } [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)] public sealed class NoAsyncTimeoutAttribute AsyncTimeoutAttribute { // Methods public NoAsyncTimeoutAttribute() base(-1) { } }
從上面給出的定義我們可以看出這兩個特性均是ActionFilter。AsyncTimeoutAttribute的構(gòu)造函數(shù)接受一個表示超時時限(以毫秒為單位)的整數(shù)作為其參數(shù),它通過重寫OnActionExecuting方法將指定的超時時限設(shè)置給當(dāng)前Controller的AsyncManager的Timeout屬性進行。NoAsyncTimeoutAttribute是AsyncTimeoutAttribute的繼承者,它將超時時限設(shè)置為-1,意味著它解除了對超時的限制。
從應(yīng)用在這兩個特性的AttributeUsageAttribute定義可看出,它們既可以應(yīng)用于類也可以用于也方法,意味著我們可以將它們應(yīng)用到Controller類型或者異步Action方法(僅對XxxAsync方法有效,不能應(yīng)用到XxxCompleted方法上)。如果我們將它們同時應(yīng)用到Controller類和Action方法上,針對方法級別的特性無疑具有更高的優(yōu)先級。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
asp.net 讀取并修改config文件實現(xiàn)代碼
讀取并修改 config 文件的asp.net代碼,方便我們用asp.net修改配置文件。2009-11-11asp.net顯示自己的網(wǎng)頁圖標(biāo)的幾種方式
多tab的瀏覽器,你一定會發(fā)現(xiàn)tab前邊的個性圖標(biāo),關(guān)于這個東西有好幾種做法,下面與大家分享下2014-05-05asp.net錯誤處理Application_Error事件示例
Application_Error事件與Page_Error事件相類似,可使用他捕獲發(fā)生在應(yīng)用程序中的錯誤。由于事件發(fā)生在整個應(yīng)用程序范圍內(nèi),因此您可記錄應(yīng)用程序的錯誤信息或處理其他可能發(fā)生的應(yīng)用程序級別的錯誤2014-01-01asp.net updatepanel 導(dǎo)致JS不能加載,而無法使用的解決方法
asp.net updatepanel 局部刷新,導(dǎo)致JS不能加載,而無法使用,而且 updatepanel會刷兩次,郁悶的,解決方法如下2013-08-08LINQ字符串向datetime 轉(zhuǎn)換時失敗的處理方法
LINQ字符串向datetime 轉(zhuǎn)換時失敗的處理方法,需要的朋友可以可以參考一下2013-03-03利用Aspose.Cells實現(xiàn)萬能導(dǎo)出功能
這篇文章主要為大家詳細(xì)介紹了利用Aspose.Cells實現(xiàn)萬能導(dǎo)出功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-12-12在ASP.NET2.0中通過Gmail發(fā)送郵件的代碼
我們有時候需要發(fā)送郵件給訪問網(wǎng)頁的用戶,例如,注冊的時候,發(fā)一確認(rèn)信什么的。那么,在ASP.NET2.0中該如果操作呢?2008-06-06獲取遠(yuǎn)程網(wǎng)頁的內(nèi)容之一(downmoon原創(chuàng))
獲取遠(yuǎn)程網(wǎng)頁的內(nèi)容之一(downmoon原創(chuàng))...2007-03-03