在上篇博客【C#客戶端的異步操作】,
我介紹了一些.net中實現(xiàn)異步操作的方法,在那篇博客中,我是站在整個.net平臺的角度來講述各種異步操作的實現(xiàn)方式,
并針對各種異步操作以及不同的編程模型給出了一些參考建議。上篇博客談到的內(nèi)容可以算是異步操作的基礎(chǔ),
今天我再來談異步,專門來談在ASP.NET平臺下的各種異步操作。在這篇博客中,我主要演示在ASP.NET中如何使用各種異步操作。
在后續(xù)博客中,我還會分析ASP.NET的源碼,解釋為什么可以這樣做,或者這樣的原因是什么,以解密內(nèi)幕的方式向您解釋這些操作的實現(xiàn)原理。
由于本文是【C#客戶端的異步操作】的續(xù)集, 因此一些關(guān)于異步的基礎(chǔ)內(nèi)容,就不再過多解釋了。如不理解本文的示例代碼,請先看完那篇博文吧。
在【C#客戶端的異步操作】的結(jié)尾,
有一個小節(jié)【在Asp.net中使用異步】,我把我上次寫好的示例做了個簡單的介紹,今天我來專門解釋那些示例代碼。
不過,在寫博客的過程中,又做了一點補充,所以,請以前下載過示例代碼的朋友,你們需要重新下載那些示例代碼(還是那篇博客中)。
說明:那些代碼都是在示范使用異步的方式調(diào)用【用Asp.net寫自己的服務(wù)框架】博客中所談到的那個服務(wù)框架,
且服務(wù)方法的代碼為:
[MyServiceMethod] public static string ExtractNumber(string str) { // 延遲3秒,模擬一個長時間的調(diào)用操作,便于客戶演示異步的效果。 System.Threading.Thread.Sleep(3000); if( string.IsNullOrEmpty(str) ) return "str IsNullOrEmpty."; return new string((from c in str where Char.IsDigit(c) orderby c select c).ToArray()); }
在ASP.NET中使用異步
我在【C#客戶端的異步操作】中提到一個觀點: 對于服務(wù)程序而言,異步處理可以提高吞吐量。什么是服務(wù)程序,簡單說來就是:可以響應(yīng)來自網(wǎng)絡(luò)請求的服務(wù)端程序。 我們熟悉的ASP.NET顯然是符合這個定義的。因此在ASP.NET程序中,適當(dāng)?shù)厥褂卯惒?/b>是可以提高服務(wù)端吞吐量的。 這里所說的適當(dāng)?shù)厥褂卯惒?/b>,一般是說:當(dāng)服務(wù)器的壓力不大且很多處理請求的執(zhí)行過程被阻塞在各種I/O等待(以網(wǎng)絡(luò)調(diào)用為主)操作上時, 而采用異步來減少阻塞工作線程的一種替代同步調(diào)用的方法。 反之,如果服務(wù)器的壓力已經(jīng)足夠大,或者沒有發(fā)生各種I/O等待,那么,在此情況下使用異步是沒有意義的。
在.net中,幾乎所有的服務(wù)編程模型都是采用線程池處理請求任務(wù)的多線程工作模式。 自然地,ASP.NET也不例外,根據(jù)【C#客戶端的異步操作】的分析, 我們就不能再使用一些將阻塞操作交給線程池的方法了。比如:委托的異步調(diào)用,直接使用線程池,都是不可取的。 直接創(chuàng)建線程也是不合適的,因此那種方式會隨著處理請求的數(shù)量增大而創(chuàng)建一大堆線程,最后也將會影響性能。 因此,最終能被選用的只用BeginXxxxx/EndXxxxx方式。不過,我要補充的是:還有基于事件通知的異步模式也是一個不錯的選擇(我會用代碼來證明), 只要它是對原始BeginXxxxx/EndXxxxx方式的包裝。
在【用Asp.net寫自己的服務(wù)框架】中, 我說過,ASP.NET處理請求是采用了一種被稱為【管線】的方式,管線由HttpApplication控制并引發(fā)的一系列事件, 由HttpHandler來處理請求,而HttpModule則更多地是一種輔助角色。 還記得我在【C#客戶端的異步操作】 總結(jié)的異步特色嗎:【一路異步到底】。 ASP.NET的處理過程要經(jīng)過它們的處理,自然它們對于請求的處理也必須要支持異步。 幸運地是,這些負(fù)責(zé)請求處理的對象都是支持異步的。今天的博客也將著重介紹它們的異步工作方式。
WebForm框架,做為ASP.NET平臺上最主要且默認(rèn)的開發(fā)框架,我自然也會全面地介紹它所支持的各種異步方式。
MVC框架從2.0開始,也開始支持異步,本文也會介紹如何在這個版本中使用異步。
該選哪個先出場呢?我想了很久,最后還是決定先請出處理請求的核心對象:HttpHandler 。
異步 HttpHandler
關(guān)于HttpHandler的接口,我在【用Asp.net寫自己的服務(wù)框架】中已有介紹, 這里就不再貼出它的接口代碼了,只想說一句:那是個同步調(diào)用接口,它并沒有異步功能。要想支持異步,則必須使用另一個接口:IHttpAsyncHandler
// 摘要: // 定義 HTTP 異步處理程序?qū)ο蟊仨殞崿F(xiàn)的協(xié)定。 public interface IHttpAsyncHandler : IHttpHandler { // 摘要: // 啟動對 HTTP 處理程序的異步調(diào)用。 // // 參數(shù): // context: // 一個 System.Web.HttpContext 對象,該對象提供對用于向 HTTP 請求提供服務(wù)的內(nèi)部服務(wù)器對象(如 Request、Response、Session // 和 Server)的引用。 // // extraData: // 處理該請求所需的所有額外數(shù)據(jù)。 // // cb: // 異步方法調(diào)用完成時要調(diào)用的 System.AsyncCallback。如果 cb 為 null,則不調(diào)用委托。 // // 返回結(jié)果: // 包含有關(guān)進程狀態(tài)信息的 System.IAsyncResult。 IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData); // // 摘要: // 進程結(jié)束時提供異步處理 End 方法。 // // 參數(shù): // result: // 包含有關(guān)進程狀態(tài)信息的 System.IAsyncResult。 void EndProcessRequest(IAsyncResult result); }
這個接口也很簡單,只有二個方法,并且與【C#客戶端的異步操作】
提到的BeginXxxxx/EndXxxxx設(shè)計方式差不多。如果這樣想,那么后面的事件就好理解了。
在.net中,異步都是建立在IAsyncResult接口之上的,而BeginXxxxx/EndXxxxx是對這個接口最直接的使用方式。
下面我們來看一下如何創(chuàng)建一個支持異步的ashx文件(注意:代碼中的注釋很重要)。
public class AsyncHandler : IHttpAsyncHandler { private static readonly string ServiceUrl = "http://localhost:22132/service/DemoService/CheckUserLogin"; public void ProcessRequest(HttpContext context) { // 注意:這個方法沒有必要實現(xiàn)。因為根本就不調(diào)用它。 // 但要保留它,因為這個方法也是接口的一部分。 throw new NotImplementedException(); } public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) { // 說明: // 參數(shù)cb是一個ASP.NET的內(nèi)部委托,EndProcessRequest方法將在那個委托內(nèi)部被調(diào)用。 LoginInfo info = new LoginInfo(); info.Username = context.Request.Form["Username"]; info.Password = context.Request.Form["Password"]; MyHttpClient<LoginInfo, string> http = new MyHttpClient<LoginInfo, string>(); http.UserData = context; // ================== 開始異步調(diào)用 ============================ // 注意:您所需要的回調(diào)委托,ASP.NET已經(jīng)為您準(zhǔn)備好了,直接用cb就好了。 return http.BeginSendHttpRequest(ServiceUrl, info, cb, http); // ============================================================== } public void EndProcessRequest(IAsyncResult ar) { MyHttpClient<LoginInfo, string> http = (MyHttpClient<LoginInfo, string>)ar.AsyncState; HttpContext context = (HttpContext)http.UserData; context.Response.ContentType = "text/plain"; context.Response.Write("AsyncHandler Result: "); try { // ============== 結(jié)束異步調(diào)用,并取得結(jié)果 ================== string result = http.EndSendHttpRequest(ar); // ============================================================== context.Response.Write(result); } catch( System.Net.WebException wex ) { context.Response.StatusCode = 500; context.Response.Write(HttpWebRequestHelper.SimpleReadWebExceptionText(wex)); } catch( Exception ex ) { context.Response.StatusCode = 500; context.Response.Write(ex.Message); } }
實現(xiàn)其實是比較簡單的,大致可以總結(jié)如下:
1. 在BeginProcessRequest()方法,調(diào)用要你要調(diào)用的異步開始方法,通常會是另一個BeginXxxxx方法。
2. 在EndProcessRequest()方法,調(diào)用要你要調(diào)用的異步結(jié)束方法,通常會是另一個EndXxxxx方法。
真的就是這么簡單。
這里要說明一下,在【C#客戶端的異步操作】中, 我演示了如何使用.net framework中的API去實現(xiàn)完整的異步發(fā)送HTTP請求的調(diào)用過程,但那個過程需要二次異步,而這個IHttpAsyncHandler接口卻只支持一次回調(diào)。 因此,對于這種情況,就需要我們自己封裝,將多次異步轉(zhuǎn)變成一次異步。以下是我包裝的一次異步的簡化版本:
下面這個包裝類非常有用,我后面的示例還將會使用它。它也示范了如何創(chuàng)建自己的IAsyncResult封裝。因此建議仔細(xì)閱讀它。 (注意:代碼中的注釋很重要)
/// <summary> /// 對異步發(fā)送HTTP請求全過程的包裝類, /// 按IAsyncResult接口要求提供BeginSendHttpRequest/EndSendHttpRequest方法(一次回調(diào)) /// </summary> /// <typeparam name="TIn"></typeparam> /// <typeparam name="TOut"></typeparam> public class MyHttpClient<TIn, TOut> { /// <summary> /// 用于保存額外的用戶數(shù)據(jù)。 /// </summary> public object UserData; public IAsyncResult BeginSendHttpRequest(string url, TIn input, AsyncCallback cb, object state) { // 準(zhǔn)備返回值 MyHttpAsyncResult ar = new MyHttpAsyncResult(cb, state); // 開始異步調(diào)用 HttpWebRequestHelper<TIn, TOut>.SendHttpRequestAsync(url, input, SendHttpRequestCallback, ar); return ar; } private void SendHttpRequestCallback(TIn input, TOut result, Exception ex, object state) { // 進入這個方法表示異步調(diào)用已完成 MyHttpAsyncResult ar = (MyHttpAsyncResult)state; // 設(shè)置完成狀態(tài),并發(fā)出完成通知。 ar.SetCompleted(ex, result); } public TOut EndSendHttpRequest(IAsyncResult ar) { if( ar == null ) throw new ArgumentNullException("ar"); // 說明:我并沒有檢查ar對象是不是與之匹配的BeginSendHttpRequest實例方法返回的, // 雖然這是不規(guī)范的,但我還是希望示例代碼能更簡單。 // 我想應(yīng)該極少有人會亂傳遞這個參數(shù)。 MyHttpAsyncResult myResult = ar as MyHttpAsyncResult; if( myResult == null ) throw new ArgumentException("無效的IAsyncResult參數(shù),類型不是MyHttpAsyncResult。"); if( myResult.EndCalled ) throw new InvalidOperationException("不能重復(fù)調(diào)用EndSendHttpRequest方法。"); myResult.EndCalled = true; myResult.WaitForCompletion(); return (TOut)myResult.Result; } } internal class MyHttpAsyncResult : IAsyncResult { internal MyHttpAsyncResult(AsyncCallback callBack, object state) { _state = state; _asyncCallback = callBack; } internal object Result { get; private set; } internal bool EndCalled; private object _state; private volatile bool _isCompleted; private ManualResetEvent _event; private Exception _exception; private AsyncCallback _asyncCallback; public object AsyncState { get { return _state; } } public bool CompletedSynchronously { get { return false; } // 其實是不支持這個屬性 } public bool IsCompleted { get { return _isCompleted; } } public WaitHandle AsyncWaitHandle { get { if( _isCompleted ) return null; // 注意這里并不返回WaitHandle對象。 if( _event == null ) // 注意這里的延遲創(chuàng)建模式。 _event = new ManualResetEvent(false); return _event; } } internal void SetCompleted(Exception ex, object result) { this.Result = result; this._exception = ex; this._isCompleted = true; ManualResetEvent waitEvent = Interlocked.CompareExchange(ref _event, null, null); if( waitEvent != null ) waitEvent.Set(); // 通知 EndSendHttpRequest() 的調(diào)用者 if( _asyncCallback != null ) _asyncCallback(this); // 調(diào)用 BeginSendHttpRequest()指定的回調(diào)委托 } internal void WaitForCompletion() { if( _isCompleted == false ) { WaitHandle waitEvent = this.AsyncWaitHandle; if( waitEvent != null ) waitEvent.WaitOne(); // 使用者直接(非回調(diào)方式)調(diào)用了EndSendHttpRequest()方法。 } if( _exception != null ) throw _exception; // 將異步調(diào)用階段捕獲的異常重新拋出。 } // 注意有二種線程競爭情況: // 1. 在回調(diào)線程中調(diào)用SetCompleted時,原線程訪問AsyncWaitHandle // 2. 在回調(diào)線程中調(diào)用SetCompleted時,原線程調(diào)用WaitForCompletion // 說明:在回調(diào)線程中,會先調(diào)用SetCompleted,再調(diào)用WaitForCompletion }
對于這個包裝類來說,最關(guān)鍵還是MyHttpAsyncResult的實現(xiàn),它是異步模式的核心。
ASP.NET 異步頁的實現(xiàn)方式
從上面的異步HttpHandler可以看到,一個處理流程被分成二個階段了。但Page也是一個HttpHandler,不過,Page在處理請求時, 有著更復(fù)雜的過程,通常被人們稱為【頁面生命周期】,一個頁面生命周期對應(yīng)著一個ASPX頁的處理過程。 對于同步頁來說,整個過程從頭到尾連續(xù)執(zhí)行一遍就行了,這比較容易理解。但是對于異步頁來說,它必須要拆分成二個階段, 以下圖片反映了異步頁的頁面生命周期。注意右邊的流程是代表異步頁的。

這個圖片是我從網(wǎng)上找的。原圖比較小,字體較模糊,我將原圖放大后又做了一番處理。本想在圖片中再加點說明, 考慮到尊重原圖作者,沒有在圖片上加上任何多余字符。下面我還是用文字來補充說明一下吧。
在上面的左側(cè)部分是一個同步頁的處理過程,右側(cè)為一個異步頁的處理過程。
這里尤其要注意的是那二個紅色塊的步驟:它們雖然只有一個Begin與End的操作,
但它們反映的是:在一個異步頁的【頁面生命周期】中,所有異步任務(wù)在執(zhí)行時所處的階段。
與HttpHandler不同,一個異步頁可以發(fā)起多個異步調(diào)用任務(wù)。
或許用所有這個詞也不太恰當(dāng),您就先理解為所有吧,后面會有詳細(xì)的解釋。
引入這個圖片只是為了能讓您對于異步頁的執(zhí)行過程有個大致的印象: 它將原來一個線程連續(xù)執(zhí)行的過程分成以PreRender和PreRenderComplete為邊界的二段過程, 且可能會由二個不同的線程來分別處理它們。請記住這個邊界,下面在演示范例時我會再次提到它們。
異步頁這個詞我已說過多次了,什么樣的頁面是一個異步頁呢?
簡單說來,異步頁并不要求您要實現(xiàn)什么接口,只要在ASPX頁的Page指令中,加一個【Async="true"】的選項就可以了,請參考如下代碼:
<%@ Page Language="C#" Async="true" AutoEventWireup="true" CodeFile="AsyncPage1.aspx.cs" Inherits="AsyncPage1" %>
很簡單吧,再來看一下CodeFile中頁面類的定義:
public partial class AsyncPage1 : System.Web.UI.Page
沒有任何特殊的,就是一個普通的頁面類。是的,但它已經(jīng)是一個異步頁了。有了這個基礎(chǔ),我們就可以為它添加異步功能了。
由于ASP.NET的異步頁有 3 種實現(xiàn)方式,我也將分別介紹它們。請繼續(xù)往下閱讀。
1. 調(diào)用Page.AddOnPreRenderCompleteAsync()的異步頁
在.net的世界里,許多支持異步的原始API都采用了Begin/End的設(shè)計方式,都是基于IAsyncResult接口的。 為了能方便地使用這些API,ASP.NET為它們設(shè)計了正好匹配的調(diào)用方式,那就是直接調(diào)用Page.AddOnPreRenderCompleteAsync()方法。 這個方法的名字也大概說明它的功能:添加一個異步操作到PreRenderComplete事件前。 我們還是來看一下這個方法的簽名吧:
// 摘要: // 為異步頁注冊開始和結(jié)束事件處理程序委托。 // // 參數(shù): // state: // 一個包含事件處理程序的狀態(tài)信息的對象。 // // endHandler: // System.Web.EndEventHandler 方法的委托。 // // beginHandler: // System.Web.BeginEventHandler 方法的委托。 // // 異常: // System.InvalidOperationException: // <async> 頁指令沒有設(shè)置為 true。- 或 -System.Web.UI.Page.AddOnPreRenderCompleteAsync(System.Web.BeginEventHandler,System.Web.EndEventHandler) // 方法在 System.Web.UI.Control.PreRender 事件之后調(diào)用。 // // System.ArgumentNullException: // System.Web.UI.PageAsyncTask.BeginHandler 或 System.Web.UI.PageAsyncTask.EndHandler // 為空引用(Visual Basic 中為 Nothing)。 public void AddOnPreRenderCompleteAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
其中BeginEventHandler與EndEventHandler的定義如下:
// 摘要: // 表示處理異步事件(如應(yīng)用程序事件)的方法。此委托在異步操作開始時調(diào)用。 // // 返回結(jié)果: // System.IAsyncResult,它表示 System.Web.BeginEventHandler 操作的結(jié)果。 public delegate IAsyncResult BeginEventHandler(object sender, EventArgs e, AsyncCallback cb, object extraData); // 摘要: // 表示處理異步事件(如應(yīng)用程序事件)的方法。 public delegate void EndEventHandler(IAsyncResult ar);
如果單看以上接口的定義,可以發(fā)現(xiàn)除了“object sender, EventArgs e”是多余部分之外,其余部分則剛好與Begin/End的設(shè)計方式完全吻合,沒有一點多余。
我們來看一下如何調(diào)用這個方法來實現(xiàn)異步的操作:(注意代碼中的注釋)
protected void button1_click(object sender, EventArgs e) { Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); // 準(zhǔn)備回調(diào)數(shù)據(jù),它將由AddOnPreRenderCompleteAsync的第三個參數(shù)被傳入。 MyHttpClient<string, string> http = new MyHttpClient<string, string>(); http.UserData = textbox1.Text; // 注冊一個異步任務(wù)。注意這三個參數(shù)哦。 AddOnPreRenderCompleteAsync(BeginCall, EndCall, http); } private IAsyncResult BeginCall(object sender, EventArgs e, AsyncCallback cb, object extraData) { // 在這個方法中, // sender 就是 this // e 就是 EventArgs.Empty // cb 就是 EndCall // extraData 就是調(diào)用AddOnPreRenderCompleteAsync的第三個參數(shù) Trace.Write("BeginCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); MyHttpClient<string, string> http = (MyHttpClient<string, string>)extraData; // 開始一個異步調(diào)用。頁面線程也最終在執(zhí)行這個調(diào)用后返回線程池了。 // 中間則是等待網(wǎng)絡(luò)的I/O的完成通知。 // 如果網(wǎng)絡(luò)調(diào)用完成,則會調(diào)用 cb 對應(yīng)的回調(diào)委托,其實就是下面的方法 return http.BeginSendHttpRequest(ServiceUrl, (string)http.UserData, cb, http); } private void EndCall(IAsyncResult ar) { // 到這個方法中,表示一個任務(wù)執(zhí)行完畢。 // 參數(shù) ar 就是BeginCall的返回值。 Trace.Write("EndCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); MyHttpClient<string, string> http = (MyHttpClient<string, string>)ar.AsyncState; string str = (string)http.UserData; try{ // 結(jié)束異步調(diào)用,獲取調(diào)用結(jié)果。如果有異常,也會在這里拋出。 string result = http.EndSendHttpRequest(ar); labMessage.Text = string.Format("{0} => {1}", str, result); } catch(Exception ex){ labMessage.Text = string.Format("{0} => Error: {1}", str, ex.Message); } }
對照一下異步HttpHandler中的介紹,你會發(fā)現(xiàn)它們非常像。
如果要執(zhí)行多個異步任務(wù),可以參考下面的代碼:
protected void button1_click(object sender, EventArgs e) { Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); MyHttpClient<string, string> http = new MyHttpClient<string, string>(); http.UserData = textbox1.Text; AddOnPreRenderCompleteAsync(BeginCall, EndCall, http); MyHttpClient<string, string> http2 = new MyHttpClient<string, string>(); http2.UserData = "T2_" + Guid.NewGuid().ToString(); AddOnPreRenderCompleteAsync(BeginCall2, EndCall2, http2); }
也很簡單,就是調(diào)用二次AddOnPreRenderCompleteAsync而已。
前面我說過,異步的處理是發(fā)生在PreRender和PreRenderComplete之間,我們來還是看一下到底是不是這樣的。 在ASP.NET的Page中,我們很容易的輸出一些調(diào)試信息,且它們會顯示在所處的頁面生命周期的相應(yīng)執(zhí)行階段中。 這個方法很簡單,在Page指令中加上【Trace="true"】選項,并在頁面類的代碼中調(diào)用Trace.Write()或者Trace.Warn()就可以了。 下面來看一下我加上調(diào)試信息的頁面執(zhí)行過程吧。

從這張圖片中,我們至少可以看到二個信息:
1. 所有的異步任務(wù)的執(zhí)行過程確實發(fā)生在PreRender和PreRenderComplete之間。
2. 所有的異步任務(wù)被串行地執(zhí)行了。
2. 調(diào)用Page.RegisterAsyncTask()的異步頁
我一直認(rèn)為ASP.NET程序也是一種服務(wù)程序,它要對客戶端瀏覽器發(fā)出的請求而服務(wù)。 由于是服務(wù),對于要服務(wù)的對象來說,都希望能盡快地得到響應(yīng),這其實也是對服務(wù)的一個基本的要求, 那就是:高吞量地快速響應(yīng)。
對于前面所說的方法,顯然,它的所有異步任務(wù)都是串行執(zhí)行的,對于客戶端來說,等待的時間會較長。 而且,最嚴(yán)重的是,如果服務(wù)超時,上面的方法會一直等待,直到本次請求超時。 為了解決這二個問題,ASP.NET定義了一種異步任務(wù)類型:PageAsyncTask 。它可以解決以上二種問題。 首先我們還是來看一下PageAsyncTask類的定義:(說明:這個類的關(guān)鍵就是它的構(gòu)造函數(shù))
// 摘要: // 使用并行執(zhí)行的指定值初始化 System.Web.UI.PageAsyncTask 類的新實例。 // // 參數(shù): // state: // 表示任務(wù)狀態(tài)的對象。 // // executeInParallel: // 指示任務(wù)能否與其他任務(wù)并行處理的值。 // // endHandler: // 當(dāng)任務(wù)在超時期內(nèi)成功完成時要調(diào)用的處理程序。 // // timeoutHandler: // 當(dāng)任務(wù)未在超時期內(nèi)成功完成時要調(diào)用的處理程序。 // // beginHandler: // 當(dāng)異步任務(wù)開始時要調(diào)用的處理程序。 // // 異常: // System.ArgumentNullException: // beginHandler 參數(shù)或 endHandler 參數(shù)未指定。 public PageAsyncTask(BeginEventHandler beginHandler, EndEventHandler endHandler, EndEventHandler timeoutHandler, object state, bool executeInParallel);
注意這個構(gòu)造函數(shù)的簽名,它與AddOnPreRenderCompleteAsync()相比,多了二個參數(shù):EndEventHandler timeoutHandler, bool executeInParallel 。 它們的含義上面的注釋中有說明,這里只是提示您要注意它們而已。
創(chuàng)建好一個PageAsyncTask對象后,只要調(diào)用頁面的RegisterAsyncTask()方法就可以注冊一個異步任務(wù)。 具體用法可參考我的如下代碼:(注意代碼中的注釋)
protected void button1_click(object sender, EventArgs e) { Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); // 準(zhǔn)備回調(diào)數(shù)據(jù),它將由PageAsyncTask構(gòu)造函數(shù)的第四個參數(shù)被傳入。 MyHttpClient<string, string> http = new MyHttpClient<string, string>(); http.UserData = textbox1.Text; // 創(chuàng)建異步任務(wù) PageAsyncTask task = new PageAsyncTask(BeginCall, EndCall, TimeoutCall, http); // 注冊異步任務(wù) RegisterAsyncTask(task); } private IAsyncResult BeginCall(object sender, EventArgs e, AsyncCallback cb, object extraData) { // 在這個方法中, // sender 就是 this // e 就是 EventArgs.Empty // cb 是ASP.NET定義的一個委托,我們只管在異步調(diào)用它時把它用作回調(diào)委托就行了。 // extraData 就是PageAsyncTask構(gòu)造函數(shù)的第四個參數(shù) Trace.Warn("BeginCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); MyHttpClient<string, string> http = (MyHttpClient<string, string>)extraData; // 開始一個異步調(diào)用。 return http.BeginSendHttpRequest(ServiceUrl, (string)http.UserData, cb, http); } private void EndCall(IAsyncResult ar) { // 到這個方法中,表示一個任務(wù)執(zhí)行完畢。 // 參數(shù) ar 就是BeginCall的返回值。 Trace.Warn("EndCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); MyHttpClient<string, string> http = (MyHttpClient<string, string>)ar.AsyncState; string str = (string)http.UserData; try { // 結(jié)束異步調(diào)用,獲取調(diào)用結(jié)果。如果有異常,也會在這里拋出。 string result = http.EndSendHttpRequest(ar); labMessage.Text = string.Format("{0} => {1}", str, result); } catch( Exception ex ) { labMessage.Text = string.Format("{0} => Error: {1}", str, ex.Message); } } private void TimeoutCall(IAsyncResult ar) { // 到這個方法,就表示任務(wù)執(zhí)行超時了。 Trace.Warn("TimeoutCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); MyHttpClient<string, string> http = (MyHttpClient<string, string>)ar.AsyncState; string str = (string)http.UserData; labMessage.Text = string.Format("{0} => Timeout.", str); }
前面我說過PageAsyncTask是支持超時的,那么它的超時功能是如何使用的呢,上面的示例只是給了一個超時的回調(diào)委托而已。
在開始演示PageAsyncTask的高級功能前,有必要說明一下示例所調(diào)用的服務(wù)端代碼。 本示例所調(diào)用的服務(wù)是【C#客戶端的異步操作】中使用的演示服務(wù), 服務(wù)代碼如下:
[MyServiceMethod] public static string ExtractNumber(string str) { // 延遲3秒,模擬一個長時間的調(diào)用操作,便于客戶演示異步的效果。 System.Threading.Thread.Sleep(3000); if( string.IsNullOrEmpty(str) ) return "str IsNullOrEmpty."; return new string((from c in str where Char.IsDigit(c) orderby c select c).ToArray()); }
下面的示例我將演示開始二個異步任務(wù),并設(shè)置異步頁的超時時間為4秒鐘。
protected void button1_click(object sender, EventArgs e) { Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); // 設(shè)置頁面超時時間為4秒 Page.AsyncTimeout = new TimeSpan(0, 0, 4); // 注冊第一個異步任務(wù) MyHttpClient<string, string> http = new MyHttpClient<string, string>(); http.UserData = textbox1.Text; PageAsyncTask task = new PageAsyncTask(BeginCall, EndCall, TimeoutCall, http); RegisterAsyncTask(task); // 注冊第二個異步任務(wù) MyHttpClient<string, string> http2 = new MyHttpClient<string, string>(); http2.UserData = "T2_" + Guid.NewGuid().ToString(); PageAsyncTask task2 = new PageAsyncTask(BeginCall2, EndCall2, TimeoutCall2, http2); RegisterAsyncTask(task2); }
此頁面的執(zhí)行過程如下:

確實,第二個任務(wù)執(zhí)行超時了。
再來看一下PageAsyncTask所支持的任務(wù)的并行執(zhí)行是如何調(diào)用的:
protected void button1_click(object sender, EventArgs e) { Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); // 設(shè)置頁面超時時間為4秒 Page.AsyncTimeout = new TimeSpan(0, 0, 4); // 注冊第一個異步任務(wù) MyHttpClient<string, string> http = new MyHttpClient<string, string>(); http.UserData = textbox1.Text; PageAsyncTask task = new PageAsyncTask(BeginCall, EndCall, TimeoutCall, http, true /*注意這個參數(shù)*/); RegisterAsyncTask(task); // 注冊第二個異步任務(wù) MyHttpClient<string, string> http2 = new MyHttpClient<string, string>(); http2.UserData = "T2_" + Guid.NewGuid().ToString(); PageAsyncTask task2 = new PageAsyncTask(BeginCall2, EndCall2, TimeoutCall2, http2, true /*注意這個參數(shù)*/); RegisterAsyncTask(task2); }
此頁面的執(zhí)行過程如下:

圖片清楚地反映出,這二個任務(wù)是并行執(zhí)行時,所以,這二個任務(wù)能在4秒內(nèi)同時執(zhí)行完畢。
在結(jié)束對PageAsyncTask的介紹前,有必要對超時做個說明。
對于使用PageAsyncTask的異步頁來說,有二種方法來設(shè)置超時時間:
1. 通過Page指令: asyncTimeout="0:00:45" ,這個值就是異步頁的默認(rèn)值。至于這個值的含義,我想您應(yīng)該懂的。
2. 通過設(shè)置 Page.AsyncTimeout = new TimeSpan(0, 0, 4); 這種方式。示例代碼就是這種方式。
注意:由于AsyncTimeout是Page級別的參數(shù),因此,它是針對所有的PageAsyncTask來限定的,并非每個PageAsyncTask的超時都是這個值。
3. 基于事件模式的異步頁
如果您看過我的博客【C#客戶端的異步操作】, 那么對【基于事件模式的異步】這個詞就不會再感到陌生了。在那篇博客中,我就對這種異步模式做過介紹, 只不是,上次是在WinForm程序中演示的而已。為了方便對比,我再次把那段代碼貼出來:
/// <summary> /// 基于事件的異步模式 /// </summary> /// <param name="str"></param> private void CallViaEvent(string str) { MyAysncClient<string, string> client = new MyAysncClient<string, string>(ServiceUrl); client.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client_OnCallCompleted); client.CallAysnc(str, str); } void client_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e) { //bool flag = txtOutput.InvokeRequired; // 注意:這里flag的值是false,也就是說可以直接操作UI界面 if( e.Error == null ) ShowResult(string.Format("{0} => {1}", e.UserState, e.Result)); else ShowResult(string.Format("{0} => Error: {1}", e.UserState, e.Error.Message)); }
上次,我就解釋過,這種方法在WinForm中非常方便。幸運的是,ASP.NET的異步頁也支持這種方式。
ASP.NET的異步頁中的實現(xiàn)代碼如下:
private void CallViaEvent(string str) { MyAysncClient<string, string> client = new MyAysncClient<string, string>(ServiceUrl); client.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client_OnCallCompleted); client.CallAysnc(str, str); } void client_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e) { Trace.Warn("client_OnCallCompleted ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); if( e.Error == null ) labMessage.Text = string.Format("{0} => {1}", e.UserState, e.Result); else labMessage.Text = string.Format("{0} => Error: {1}", e.UserState, e.Error.Message); }
搞什么呀,這二段代碼是一樣的嘛。 您是不是也有這樣的感覺呢?
仔細(xì)看這二段代碼,還是能發(fā)現(xiàn)它們有區(qū)別的。這里我就不指出它們了。它們與異步無關(guān),說出它們意義不大, 反而,我更希望您對【基于事件模式的異步】留個好印象:它們就是一樣的。
再來看一下如何發(fā)出多個異步任務(wù):
protected void button1_click(object sender, EventArgs e) { Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); string str = textbox1.Text; // 注意:這個異步任務(wù),我設(shè)置了2秒的超時。它應(yīng)該是不能按時完成任務(wù)的。 MyAysncClient<string, string> client = new MyAysncClient<string, string>(ServiceUrl, 2000); client.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client_OnCallCompleted); client.CallAysnc(str, str); // 開始第一個異步任務(wù) string str2 = "T2_" + Guid.NewGuid().ToString(); MyAysncClient<string, string> client2 = new MyAysncClient<string, string>(ServiceUrl); client2.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client2_OnCallCompleted); client2.CallAysnc(str2, str2); // 開始第二個異步任務(wù) } void client2_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e) { ShowCallResult(2, e); // 再來一個異步調(diào)用 string str3 = "T3_" + Guid.NewGuid().ToString(); MyAysncClient<string, string> client3 = new MyAysncClient<string, string>(ServiceUrl); client3.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client3_OnCallCompleted); client3.CallAysnc(str3, str3); // 開始第三個異步任務(wù) }
頁面的執(zhí)行過程如下圖:

這里要說明一下了:在【C#客戶端的異步操作】中我就給出這個類的實現(xiàn)代碼, 不過,這次我給它增加了超時功能,增加了一個重載的構(gòu)造函數(shù),需要在構(gòu)造函數(shù)的第二個參數(shù)傳入。 今天我就不貼出那個類的代碼了,有興趣的自己去下載代碼閱讀吧。 在上次貼的代碼,你應(yīng)該可以發(fā)現(xiàn),在CallAysnc()時,就已經(jīng)開始了異步操作。對于本示例來說,也就是在button1_click就已經(jīng)開始了二個異步操作。
這是個什么意思呢?
可以這樣來理解:前二個任務(wù)顯然是和LoadComplete,PreRender事件階段的代碼在并行執(zhí)行的。
有意思的是:第三個任務(wù)是在第二個任務(wù)的結(jié)束事件中開始的,但三個任務(wù)的結(jié)束操作全在頁面的PreRender事件才得到處理。
下面我再把這個例子來改一下,就更有趣了:
protected void button1_click(object sender, EventArgs e) { Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString()); string str = textbox1.Text; // 注意:這個異步任務(wù),我設(shè)置了2秒的超時。它應(yīng)該是不能按時完成任務(wù)的。 MyAysncClient<string, string> client = new MyAysncClient<string, string>(ServiceUrl, 2000); client.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client_OnCallCompleted); client.CallAysnc(str, str); // 開始第一個異步任務(wù) System.Threading.Thread.Sleep(3000); string str2 = "T2_" + Guid.NewGuid().ToString(); MyAysncClient<string, string> client2 = new MyAysncClient<string, string>(ServiceUrl); client2.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client2_OnCallCompleted); client2.CallAysnc(str2, str2); // 開始第二個異步任務(wù) }
現(xiàn)在,在第一個任務(wù)發(fā)出后,我讓線程等待了3秒,也就是等到了第一個任務(wù)的超時。然后再開始第二個任務(wù)。
也就是說:在button1_click事件還沒執(zhí)行完畢,第一個任務(wù)就結(jié)束了。
現(xiàn)在,您可以猜一下,此時的執(zhí)行過程是個什么樣的。
猜好了就來看下圖吧。

現(xiàn)在明白了吧:哪怕是在PostBackEvent階段就結(jié)束的任務(wù),也要等到PreRender之后才能得到處理。
至于為什么會是這樣的,我以后再講。今天只要記住本文的第一張圖片就好了。
我可是好不容易才找出這張圖片來的,且為了讓您能看得更清楚,還花了些時間修改了它。
在那個圖片后面我還說過:在一個異步頁的【頁面生命周期】中,所有異步任務(wù)在執(zhí)行時所處的階段。
并在后面注明了這里的所有這個詞也不太恰當(dāng)。現(xiàn)在可以解釋為什么不恰當(dāng)了:
【基于事件模式的異步】的開始階段并不一定要PreRender事件之后,而對于前二種異步面的實現(xiàn)方式則是肯定在PreRender事件之后。
至于這其中的原因,同樣,您要等待我的后續(xù)博客了。
各種異步頁的實現(xiàn)方式比較
前面介紹了3種異步頁的實現(xiàn)方式,我打算在這里給它們做個總結(jié)及比較。當(dāng)然,這一切只代表我個人的觀點,僅供參考。
為了能給出一個客觀的評價,我認(rèn)為先有必要再給個示例,把這些異步方式放在一起執(zhí)行,就好像把它們放在一起比賽一樣, 或許這樣會更有意思,同時也會讓我給出的評價更有說服力。
在下面的示例中,我把上面說過的3種異步方式放在一起,并讓每種方法執(zhí)行多次(共10個異步任務(wù)),實驗代碼如下:
protected void button1_click(object sender, EventArgs e) { ShowThreadInfo("button1_click"); // 為PageAsyncTask設(shè)置超時時間 Page.AsyncTimeout = new TimeSpan(0, 0, 7); // 開啟4個PageAsyncTask,其中第1,4個任務(wù)不接受并行執(zhí)行,2,3則允許并行執(zhí)行 Async_RegisterAsyncTask("RegisterAsyncTask_1", false); Async_RegisterAsyncTask("RegisterAsyncTask_2", true); Async_RegisterAsyncTask("RegisterAsyncTask_3", true); Async_RegisterAsyncTask("RegisterAsyncTask_4", false); // 開啟3個AddOnPreRenderCompleteAsync的任務(wù) Async_AddOnPreRenderCompleteAsync("AddOnPreRenderCompleteAsync_1"); Async_AddOnPreRenderCompleteAsync("AddOnPreRenderCompleteAsync_2"); Async_AddOnPreRenderCompleteAsync("AddOnPreRenderCompleteAsync_3"); // 最后開啟3個基于事件通知的異步任務(wù),其中第2個任務(wù)由于設(shè)置了超時,將不能成功完成。 Async_Event("MyAysncClient_1", 0); Async_Event("MyAysncClient_2", 2000); Async_Event("MyAysncClient_3", 0); } private void Async_RegisterAsyncTask(string taskName, bool executeInParallel) { MyHttpClient<string, string> http = new MyHttpClient<string, string>(); http.UserData = taskName; PageAsyncTask task = new PageAsyncTask(BeginCall_Task, EndCall_Task, TimeoutCall_Task, http, executeInParallel); RegisterAsyncTask(task); } private void Async_AddOnPreRenderCompleteAsync(string taskName) { MyHttpClient<string, string> http = new MyHttpClient<string, string>(); http.UserData = taskName; AddOnPreRenderCompleteAsync(BeginCall, EndCall, http); } private void Async_Event(string taskName, int timeoutMilliseconds) { MyAysncClient<string, string> client = new MyAysncClient<string, string>(ServiceUrl, timeoutMilliseconds); client.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client_OnCallCompleted); client.CallAysnc(taskName, taskName); }
執(zhí)行過程如下圖:

不知您看到這個執(zhí)行過程是否會想到為什么會是這個樣子的。至于為什么會是這個樣子的, 這就涉及到ASP.NET的異步頁的執(zhí)行過程,這個過程比較復(fù)雜,我以后再談。 今天咱們就來根據(jù)這個圖片來談?wù)劚容^表面化的東西,談一下這三種方式的差別。
從上面的代碼以及執(zhí)行過程,可以看到一個有趣的現(xiàn)象,我明明是先注冊的4個PageAsyncTask 。 可是呢,最先顯示的卻是【BeginCall AddOnPreRenderCompleteAsync_1】。 我想我這里使用顯示這個詞也是比較恰當(dāng)?shù)模瑸槭裁茨兀恳驗椋仪懊嬉呀?jīng)解釋過了, 基于事件的異步的任務(wù)應(yīng)該是在button1_click事件處理器中先執(zhí)行的,只是我沒有讓它們顯示罷了。 接下來的故事也很自然,由于我將"MyAysncClient_2"設(shè)置為2秒的超時,它最先完成,只是結(jié)果為超時罷了。 緊接著,"MyAysncClient_1"和"MyAysncClient_3"也執(zhí)行結(jié)束了。嗯,是的:3個事件的異步任務(wù)全執(zhí)行完了。
說到這里我要另起一段了,以提醒您的注意。
有沒有注意到,前面說到的3個事件的異步任務(wù)全執(zhí)行完了。這個時候,其它的異步任務(wù)絕大部分還沒有開始呢,
它們3個咋就先執(zhí)行完了呢?
有意思吧,其實何止3個,如果再來5個基于事件的異步任務(wù),它們還是會先執(zhí)行完成,不信的話,看下圖:

或許舉這個例子把基于事件的異步方式捧高了。這里我也要客觀的解釋一下原因了:
出現(xiàn)這個現(xiàn)象主要由2個原因造成的:
1. 在這個例子中,"MyAysncClient_1", "MyAysncClient_2", "MyAysncClient_3", "AddOnPreRenderCompleteAsync_1"
由于都是異步任務(wù),所以基本上是并行執(zhí)行的,
2. 由于3個基于事件的異步方式先執(zhí)行的,因此它們先結(jié)束了。
接著來解釋圖片所反映的現(xiàn)象。當(dāng)基于事件的異步任務(wù)全執(zhí)行完成后," EndCall AddOnPreRenderCompleteAsync_1" 也被調(diào)用了。說明"AddOnPreRenderCompleteAsync_1"這個任務(wù)徹底地執(zhí)行完了。 接下來,"AddOnPreRenderCompleteAsync_2","AddOnPreRenderCompleteAsync_3"也依次執(zhí)行完了。
我一開始用RegisterAsyncTask注冊的4個異步任務(wù)呢?終于,在前面的所有異步任務(wù)全部執(zhí)行完成后, 才開始了這類任務(wù)的執(zhí)行過程。首先執(zhí)行的是"RegisterAsyncTask_1",這個好理解。 接下來,"BeginCall RegisterAsyncTask_2", "BeginCall RegisterAsyncTask_3"被連續(xù)調(diào)用了, 這也好理解吧,因為我當(dāng)時創(chuàng)建異步任務(wù)時,指定它們是允許與其它任務(wù)并行執(zhí)行的,因此它們是一起執(zhí)行的。 3秒后,2個任務(wù)同時執(zhí)行完了,最后啟動了"RegisterAsyncTask_4",由于它不支持并行執(zhí)行,所以,它排在最后, 在沒有任何懸念中,"TimeoutCall RegisterAsyncTask_4"被調(diào)用了。這么正常啊,我設(shè)置過Page.AsyncTimeout = new TimeSpan(0, 0, 7); 因此,前二批PageAsyncTask趕在超時前正常結(jié)束了,留給"RegisterAsyncTask_4"的執(zhí)行時間只有1秒,它當(dāng)然就不能在指定時間內(nèi)正常完成。
似乎到這里,這些異步任務(wù)的執(zhí)行過程都解釋完了,但是,有二個很奇怪的現(xiàn)象您有沒有發(fā)現(xiàn):
1. 為什么AddOnPreRenderCompleteAsync的任務(wù)全執(zhí)行完了之后,才輪到PageAsyncTask的任務(wù)呢?
2. 還有前面說過的,為什么是"BeginCall AddOnPreRenderCompleteAsync_1"最先顯示呢?
這一切絕非偶然,如果您有興趣,可下載我的示例代碼,你運行千遍萬遍還將是這個結(jié)果。
這些原因我以后再談,今天的博客只是想告訴您這樣一個結(jié)果就行了。
不過,為了能讓您能容易地理解后面的內(nèi)容,我暫且告訴您:PageAsyncTask是建立在AddOnPreRenderCompleteAsync的基礎(chǔ)上的。
有了前面這些實驗結(jié)果,我們再來對這3種異步頁方法做個總結(jié)及比較。
1. AddOnPreRenderCompleteAsync: 它提供了最基本的異步頁的使用方法。就好像HttpHandler一樣,它雖能處理請求,但不太方便,顯得比較原始。 由于它提供的是比較原始的方法,您也可以自行包裝您的高級功能。
2. PageAsyncTask: 與AddOnPreRenderCompleteAsync相比,它增加了超時以及并行執(zhí)行的功能,但我也說過,它是建立在AddOnPreRenderCompleteAsync的基礎(chǔ)之上的。 如果把AddOnPreRenderCompleteAsync比作為HttpHandler,那么PageAsyncTask則就像是Page 。因此它只是做了些高級的包裝罷了。
3. 基于事件的異步方式:與前2者完全沒有關(guān)系,它只依賴于AspNetSynchronizationContext。這里有必要強調(diào)一下: 【基于事件的異步方式】可以理解為一個設(shè)計模式,也可以把它理解成對最基礎(chǔ)的異步方式的高級包裝。 它能提供或者完成的功能,依賴于包裝的方式及力度。 在我提供的這個包裝類中,它也可以實現(xiàn)與PageAsyncTask一樣的并行執(zhí)行以及超時功能。
后二種方法功能強大的原因是來源于高級包裝,由于包裝,過程也會更復(fù)雜,因此性能或許也會有微小的損失。 如果您不能接受這點性能損失,可能還是選AddOnPreRenderCompleteAsync會比較合適。 不過,我要再次提醒您:它不支持并行執(zhí)行,不支持超時。
請容忍我再夸一下【基于事件的異步模式】,從我前面的示例代碼,尤其是與WinForm中的示例代碼的比較中, 我們可以清楚的發(fā)現(xiàn),這種方式是非常易用的。掌握了這種方式,至少在這二大編程模型中都是適用的。 而且,它能在異步頁的執(zhí)行周期中,較早的進入異步等待狀態(tài),因此能更快的結(jié)束執(zhí)行過程。 想想【從"Begin Raise PostBackEvent"到"End PreRender"這中間還可以執(zhí)行多少代碼是不確定的】吧。
【基于事件的異步模式】的優(yōu)點不僅如此,我的演示代碼中還演示了另一種用法: 在一個完成事件中,我還能再開啟另一個異步任務(wù)。 這個優(yōu)點使我可以有選擇性地啟動后續(xù)的異步操作。但是,這個特性是另2個不可能做到的! 這個原因可以簡單地表達(dá)為:在PreRender事件后,調(diào)用AddOnPreRenderCompleteAsync會拋異常。
異步HttpModule的實現(xiàn)方式
在【用Asp.net寫自己的服務(wù)框架】中, 我示范過如果編寫一個HttpModule,通常只要我們實現(xiàn)IHttpModule接口,并在Init方法中訂閱一些事件就可以了:
internal class DirectProcessRequestMoudle : IHttpModule { public void Init(HttpApplication app) { app.PostAuthorizeRequest += new EventHandler(app_PostAuthorizeRequest); }
HttpHandler有異步接口的IHttpAsyncHandler,但HttpModule卻只有一個接口:IHttpModule,不管是同步還是異步。 異步HttpModule的實現(xiàn)方式并不是訂閱HttpApplication的事件,而是調(diào)用HttpApplication的一些注冊異步操作的方法來實現(xiàn)的(還是在Init事件中), 這些方法可參考以下列表:
// 將指定的 System.Web.HttpApplication.AcquireRequestState 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.AcquireRequestState事件處理程序的集合。 public void AddOnAcquireRequestStateAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.AuthenticateRequest 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.AuthenticateRequest事件處理程序的集合。 public void AddOnAuthenticateRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.AuthorizeRequest 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.AuthorizeRequest事件處理程序的集合。 public void AddOnAuthorizeRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.BeginRequest 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.BeginRequest事件處理程序的集合。 public void AddOnBeginRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.EndRequest 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.EndRequest事件處理程序的集合。 public void AddOnEndRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); public void AddOnLogRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); public void AddOnMapRequestHandlerAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.PostAcquireRequestState 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.PostAcquireRequestState事件處理程序的集合。 public void AddOnPostAcquireRequestStateAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.PostAuthenticateRequest 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.PostAuthenticateRequest事件處理程序的集合。 public void AddOnPostAuthenticateRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.PostAuthorizeRequest 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.PostAuthorizeRequest事件處理程序的集合。 public void AddOnPostAuthorizeRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); public void AddOnPostLogRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.PostMapRequestHandler 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.PostMapRequestHandler事件處理程序的集合。 public void AddOnPostMapRequestHandlerAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.PostReleaseRequestState 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.PostReleaseRequestState事件處理程序的集合。 public void AddOnPostReleaseRequestStateAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.PostRequestHandlerExecute 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.PostRequestHandlerExecute事件處理程序的集合。 public void AddOnPostRequestHandlerExecuteAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.PostResolveRequestCache 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.PostResolveRequestCache事件處理程序的集合。 public void AddOnPostResolveRequestCacheAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.PostUpdateRequestCache 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.PostUpdateRequestCache事件處理程序的集合。 public void AddOnPostUpdateRequestCacheAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.PreRequestHandlerExecute 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.PreRequestHandlerExecute事件處理程序的集合。 public void AddOnPreRequestHandlerExecuteAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.ReleaseRequestState 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.ReleaseRequestState事件處理程序的集合。 public void AddOnReleaseRequestStateAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.ResolveRequestCache 事件處理程序 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.ResolveRequestCache事件處理程序的集合。 public void AddOnResolveRequestCacheAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state); // 將指定的 System.Web.HttpApplication.UpdateRequestCache 事件 // 添加到當(dāng)前請求的異步 System.Web.HttpApplication.UpdateRequestCache事件處理程序的集合。 public void AddOnUpdateRequestCacheAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
每個方法的含義從它們的名字是可以看出。
異步HttpModule的實現(xiàn)方式需要將異步對應(yīng)的Begin/End二個方法分別做為委托參數(shù)傳入這些方法中。
注意:這些方法的簽名與Page.AddOnPreRenderCompleteAsync()是一致的,因此它們的具體用法也與Page.AddOnPreRenderCompleteAsync()一樣。
為什么這里不設(shè)計成訂閱事件的方式?
我想是因為:如果采用事件模式,調(diào)用者可以只訂閱其中的一個事件,ASP.NET不容易控制,還有"object state"這個參數(shù)不便于在訂閱事件時傳入。
異步HttpModule的示例代碼如下:
/// <summary> /// 【示例代碼】演示異步的HttpModule /// 說明:這個示例一丁點意義也沒有,純粹是為了演示。 /// </summary> public class MyAsyncHttpModule : IHttpModule { public static readonly object HttpContextItemsKey = new object(); private static readonly string s_QueryDatabaseListScript = @"select dtb.name from master.sys.databases as dtb order by 1"; private static readonly string s_ConnectionString = @"server=localhost\sqlexpress;Integrated Security=SSPI;Asynchronous Processing=true"; public void Init(HttpApplication app) { // 注冊異步事件 app.AddOnBeginRequestAsync(BeginCall, EndExecuteReader, null); } private IAsyncResult BeginCall(object sender, EventArgs e, AsyncCallback cb, object extraData) { SqlConnection connection = new SqlConnection(s_ConnectionString); connection.Open(); SqlCommand command = new SqlCommand(s_QueryDatabaseListScript, connection); CallbackParam cbParam = new CallbackParam { Command = command, Context = HttpContext.Current }; return command.BeginExecuteReader(cb, cbParam); } private class CallbackParam { public SqlCommand Command; public HttpContext Context; } private void EndExecuteReader(IAsyncResult ar) { CallbackParam cbParam = (CallbackParam)ar.AsyncState; StringBuilder sb = new StringBuilder(); try { using( SqlDataReader reader = cbParam.Command.EndExecuteReader(ar) ) { while( reader.Read() ) { sb.Append(reader.GetString(0)).Append("; "); } } } catch( Exception ex ) { cbParam.Context.Items[HttpContextItemsKey] = ex.Message; } finally { cbParam.Command.Connection.Close(); } if( sb.Length > 0 ) cbParam.Context.Items[HttpContextItemsKey] = "數(shù)據(jù)庫列表:" + sb.ToString(0, sb.Length - 2); } public void Dispose() { } }
頁面可以使用如下方式獲得MyAsyncHttpModule的結(jié)果:
public partial class TestMyAsyncHttpModule : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { string result = (string)HttpContext.Current.Items[MyAsyncHttpModule.HttpContextItemsKey] ?? "沒有開啟MyAsyncHttpModule,請在web.config中啟用它。"; Response.Write(result); } }
說明:管線處理過程中,可能有多個HttpModule,但是異步的HttpModule在執(zhí)行時,只是在一個階段內(nèi),所有的HttpModule采用異步方式工作。 當(dāng)進入下一個階段前,必須要等到所有HttpModule全部在當(dāng)前階段內(nèi)執(zhí)行完畢。
通常情況下,是沒有必要寫異步的HttpModule的。這是我寫的第一個異步HttpModule。
異步的 Web Service
由于Web Service也是受ASP.NET支持,且隨著ASP.NET一起出現(xiàn)。我們再來看一下如果將一個同步的服務(wù)方法改變成異步的方法。
注意:將方法由同步改成異步版本,是不影響客戶端的。
以下代碼是一個同步版本的服務(wù)方法:
[WebMethod] public string ExtractNumber(string str) { //return ........ }
再來看一下最終的異步實現(xiàn)版本:
[WebMethod] public IAsyncResult BeginExtractNumber(string str, AsyncCallback cb, object state) { MyHttpClient<string, string> http = new MyHttpClient<string, string>(); http.UserData = "Begin ThreadId: " + Thread.CurrentThread.ManagedThreadId.ToString(); return http.BeginSendHttpRequest(ServiceUrl, str, cb, http); } [WebMethod] public string EndExtractNumber(IAsyncResult ar) { MyHttpClient<string, string> http = (MyHttpClient<string, string>)ar.AsyncState; try{ return http.EndSendHttpRequest(ar) + ", " + http.UserData.ToString() + ", End ThreadId: " + Thread.CurrentThread.ManagedThreadId.ToString(); } catch(Exception ex){ return ex.ToString(); } }
其實,要做的修改與IHttpHandler到IHttpAsyncHandler的工作差不多,在原有的同步方法后面加二個與異步操作有關(guān)的參數(shù), 并且返回值改為IAsyncResult,然后再添加一個EndXxxx方法就可以了,當(dāng)然了,EndXxxx方法的傳入?yún)?shù)只能是一個IAsyncResult類型的參數(shù)。
ASP.NET MVC 中的異步方式
在ASP.NET MVC框架中,感覺一下回到原始社會中,簡直和異步頁的封裝沒法比。來看代碼吧。(注意代碼中的注釋)
// 實際可處理的Action名稱為 Test1 ,注意名稱后要加上 Async public void Test1Async() { // 告訴ASP.NET MVC,要開始一個異步操作了。 AsyncManager.OutstandingOperations.Increment(); string str = Guid.NewGuid().ToString(); MyAysncClient<string, string> client = new MyAysncClient<string, string>(ServiceUrl); client.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client_OnCallCompleted); client.CallAysnc(str, str); // 開始異步調(diào)用 } void client_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e) { // 告訴ASP.NET MVC,一個異步操作結(jié)束了。 AsyncManager.OutstandingOperations.Decrement(); if( e.Error == null ) AsyncManager.Parameters["result"] = string.Format("{0} => {1}", e.UserState, e.Result); else AsyncManager.Parameters["result"] = string.Format("{0} => Error: {1}", e.UserState, e.Error.Message); // AsyncManager.Parameters["result"] 用于寫輸出結(jié)果。 // 這里仍然采用類似ViewData的設(shè)計。 // 注意:key 的名稱要和Test1Completed的參數(shù)名匹配。 } // 注意名稱后要加上 Completed ,且其余部分與Test1Async的前綴對應(yīng)。 public ActionResult Test1Completed(string result) { ViewData["result"] = result; return View(); }
說明:如果您認(rèn)為單獨為事件處理器寫個方法看起來不爽,您也可以采用匿名委托之類的閉包寫法,這個純屬個人喜好問題。
再來個多次異步操作的示例:
public void Test2Async() { // 表示要開啟3個異步操作。 // 如果把這個數(shù)字設(shè)為2,極有可能會產(chǎn)生的錯誤的結(jié)果。不信您可以試一下。 AsyncManager.OutstandingOperations.Increment(3); string str = Guid.NewGuid().ToString(); MyAysncClient<string, string> client = new MyAysncClient<string, string>(ServiceUrl); client.UserData = "result1"; client.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client2_OnCallCompleted); client.CallAysnc(str, str); // 開始第一個異步任務(wù) string str2 = "T2_" + Guid.NewGuid().ToString(); MyAysncClient<string, string> client2 = new MyAysncClient<string, string>(ServiceUrl); client2.UserData = "result2"; client2.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client2_OnCallCompleted); client2.CallAysnc(str2, str2); // 開始第二個異步任務(wù) string str3 = "T3_" + Guid.NewGuid().ToString(); MyAysncClient<string, string> client3 = new MyAysncClient<string, string>(ServiceUrl); client3.UserData = "result3"; client3.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client2_OnCallCompleted); client3.CallAysnc(str3, str3); // 開始第三個異步任務(wù) } void client2_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e) { // 遞減內(nèi)部的異步任務(wù)累加器。有點類似AspNetSynchronizationContext的設(shè)計。 AsyncManager.OutstandingOperations.Decrement(); MyAysncClient<string, string> client = (MyAysncClient<string, string>)sender; string key = client.UserData.ToString(); if( e.Error == null ) AsyncManager.Parameters[key] = string.Format("{0} => {1}", e.UserState, e.Result); else AsyncManager.Parameters[key] = string.Format("{0} => Error: {1}", e.UserState, e.Error.Message); } public ActionResult Test2Completed(string result1, string result2, string result3) { ViewData["result1"] = result1; ViewData["result2"] = result2; ViewData["result3"] = result3; return View(); }
我來解釋一下上面的代碼是如何以異步方式工作的。首先,我們要把Controller的基類修改為AsyncController,代碼如下:
public class HomeController : AsyncController
假如我有一個同步的Action方法:Test1,它看起來應(yīng)該是這樣的:
public ActionResult Test1() { return View(); }
首先,我需要把它的返回值改成void, 并把方法名稱修改為Test1Async 。
然后,在開始異步調(diào)用前,調(diào)用AsyncManager.OutstandingOperations.Increment();
在異步完成時:
1. 要調(diào)用AsyncManager.OutstandingOperations.Decrement();
2. 將結(jié)果寫入到AsyncManager.Parameters[]這個集合中。注意key的名字后面要用到。
到這里,異步開發(fā)的任務(wù)算是做了一大半了。你可能會想我在哪里返回ActionResult呢?
再來創(chuàng)建一個Test1Completed方法,簽名應(yīng)該是這個樣子的:
public ActionResult Test1Completed(string result)
注意:方法中的參數(shù)名要和前面說過的寫AsyncManager.Parameters[]的key名一致,包括數(shù)量。
再后面的事情,我想您懂的,我就不多說了。
再來說說我對【ASP.NET MVC的異步方式】這個設(shè)計的感受吧。
簡單說來就是:不夠完美。
要知道在這個例子中,我可是采用的基于事件的異步模式啊,在異步頁中,哪有這些額外的調(diào)用?
對于這個設(shè)計,我至少有2點不滿意:
1. AsyncManager.OutstandingOperations.Increment(); Decrement();由使用者來控制,容易出錯。
2. AsyncManager.Parameters[]這個bag設(shè)計方式也不爽,難道僅僅是為了簡單?因為我可以在完成事件時,根據(jù)條件繼續(xù)后面的異步任務(wù),最終結(jié)果可能并不確定,因此后面的XXXXCompleted方法的簽名就是個問題了。
為什么在ASP.NET MVC中,這個示例需要調(diào)用Increment(); Decrement(),而在異步頁中不需要呢?
恐怕有些人會對此有好奇,我就告訴大家吧:這與AspNetSynchronizationContext有關(guān)。
AspNetSynchronizationContext,真是個【成也蕭何,敗成蕭何】的東西,在異步頁為什么不需要我們調(diào)用類似Increment(); Decrement()的語句是因為, 它內(nèi)部也有個這樣的累加器,不過,當(dāng)時在設(shè)計基于事件的異步模式時,在ASP.NET運行環(huán)境中,SynchronizationContext就是使用了AspNetSynchronizationContext這個具體實現(xiàn)類, 但它的絕大部分成員卻是internal類型的。如果可以使用它,可以用一種簡便地方式設(shè)置一個統(tǒng)一的回調(diào)委托:
if( this._syncContext.PendingOperationsCount > 0 ) { this._syncContext.SetLastCompletionWorkItem(this._callHandlersThreadpoolCallback); }
就這么一句話,可以不用操心使用者到底開始了多少個異步任務(wù),都可以在所有的異步結(jié)束后,回調(diào)指定的委托。只是可惜的是,這二個成員都是internal的!
如果當(dāng)初微軟設(shè)計AspNetSynchronizationContext時,不開放SetLastCompletionWorkItem這個方法, 是擔(dān)心使用者亂調(diào)用導(dǎo)致ASP.NET運行錯誤的話,現(xiàn)在ASP.NET MVC的這種設(shè)計顯然更容易出錯。 當(dāng)然了,ASP.NET MVC出來的時候,這一切早就出現(xiàn)了,因此它也無法享受AspNetSynchronizationContext的便利性。 不過,最讓我想不通的是:直到ASP.NET 4.0,這一切還是原樣。 難道是因為ASP.NET MVC獨立在升級,連InternalsVisibleTo的機會也不給它嗎?
就算我們不用基于事件的異步模式,異步頁還有二種實現(xiàn)方法呢(都不需要累加器),可是ASP.NET MVC卻沒有實現(xiàn)類似的功能。 所以,這樣就顯得很不完善。我們也只能期待未來的版本能改進這些問題了。
MSDN參考文章:在 ASP.NET MVC 中使用異步控制器
受爭論的【基于事件的異步模式】
本來在我的寫作計劃中,是沒有這段文字的,可就在我打算發(fā)布這篇博客之前,想到上篇博客中的評論,突然我想到一本書:CLR via C# 。 是的,就是這本書,我想很多人手里有這本書,想到這本書是因為上篇博客的評論中,出現(xiàn)一個與我的觀點有著不一致的聲音(來自AndersTan),而他應(yīng)該是Jeffer Richter的粉絲。 我早就買了這本書了(中文第三版),其實也是AndersTan推薦的,不過一直沒有看完, 因此,根本就沒有發(fā)現(xiàn)Jeffer Richter是【基于事件的異步模式】的反對者, 這個可參考書中696頁。Jeffer Richter在書中說:“由于我不是EAP的粉絲,而且我不贊同使用這個模式, 所以一直沒有花太多的時間在它上面。然而,我知道有一些人確實喜歡這個模式,而且想使用它,所以我專門花了一些時間研究它。” 為了表示對大牛的敬重,我用藍(lán)色字體突出他說的話(當(dāng)然是由周靖翻譯的)。看到這句話以及后面他對于此模式的評價,尤其是在 【27.11.2 APM和EAP的對比】這個小節(jié)中對于EAP的評價,讓我感覺大牛其實也沒有很好地了解這個模式。
這里再補充一下,書中提到二個英文簡寫:EAP: Event-base Asynchronous Pattern, APM: Asynchronous Programming Model 。書中689頁中,Jeffer Richter還說過:“雖然我是APM的超級粉絲,但是我必須承認(rèn)它存在的一些問題。” 與之相反,雖然我不是APM的忠實粉絲,我卻不認(rèn)為他所說的問題真的是APM的缺點。他說的第一點,感覺就沒有意義。 我不知道有多少人在現(xiàn)實使用中,是在調(diào)用了Begin方法后,立即去調(diào)用End方法? 我認(rèn)為.net允許這種使用方式,可能還是更看中的是使用上的靈活性,畢竟微軟要面對的開發(fā)者會有千奇百怪的要求。 而且MSDN中也解釋了這種調(diào)用會阻塞線程。訪問IAsyncResult是可以得到一個WaitHandle對象, 這個好像在上篇博客的評論中有人也提過了,我當(dāng)時也不想說了,這次就把我的實現(xiàn)方式貼出來了,只希望告訴一些人:這個成員雖然是個耗資源的東西, 但要看你如何去實現(xiàn)它了:有些時候(異步完成的時候)可以返回null的,所以,通常應(yīng)該設(shè)計成一種延遲創(chuàng)建模式才對(我再一次的提醒:在設(shè)計它時要考慮多線程的并發(fā)訪問)。
剛才扯遠(yuǎn)了,我們還是來說關(guān)于Jeffer Richter對于【27.11.2 APM和EAP的對比】這個小節(jié)的看法(699頁)。這個小節(jié)有4個段話,分別從4個方面說了些EAP的【缺點】, 我也將依次來發(fā)表我的觀點。
1. Jeffer Richter認(rèn)為EAP的最大優(yōu)點在于和IDE的配合使用,且在后面一直提到GUI線程。 顯然EAP模式被代表了,被WinForm這類桌面程序程序代表了。 我今天的示例代碼全部是可以在ASP.NET環(huán)境下運行的,而且還特意和WinForm下的使用方法做了比較,結(jié)果是:使用方式基本相同。 我認(rèn)為這個結(jié)果才是EAP模式最大的優(yōu)點:在不同的編程模型中不必考慮線程模型問題。
2. Jeffer Richter說:事實上,EAP必須為引發(fā)的所有進度報告和完成事件分配從EventArgs派生的對象......。
看到這句話的感覺還是和上句話差不多:被代表了。 對于這段話,我認(rèn)為有必要從幾個角度來表達(dá)我的觀點:
a. 進度報告:我想問一句:ASP.NET編程模型下進度報告有什么意義,或者說如何實現(xiàn)?
在我今天演示的示例代碼中,我一直沒演示進度報告吧?事實上,我的包裝類中根本就不提供這個功能,只提供了完成事件的通知功能。
再說,為什么需要進度報告?因為桌面程序需要,它們?yōu)榱四茏尦绦驌碛懈玫挠脩趔w驗。當(dāng)然也可以不提供進度報告嘛,
大不了讓用戶守在電腦面前傻等就是了,這樣還會有性能損失嗎?當(dāng)然沒有,但是用戶可能會罵人......。
b. 性能損失:MyAysncClient是對一個更底層的靜態(tài)方法調(diào)用的封裝。我也很明白:有封裝就有性能的損失。但我想:一次異步任務(wù)也就只通知一次,性能損失能有多大?
而且明知道有性能損失,我為什么還要封裝呢?只為一個很簡單的理由:使用起來更容易!
c. 對象的回收問題:如果按照J(rèn)effer Richter的說法,多創(chuàng)建這幾個對象就讓GC為難的話,會讓我對.NET失去信心,連ASP.NET也不敢用了,
因為:要知道.NET的世界是完全面向?qū)ο蟮氖澜纾淮蜽EB請求的處理過程中,ASP.NET不知道要創(chuàng)建多少個對象,我真的數(shù)不清楚。
3. Jeffer Richter說:如果在登記事件處理方法之前調(diào)用XxxAsync方法,......。看到這里,我笑了。 顯然,大牛是非常討厭EAP模式的。EAP是使用了事件,這個錯誤的調(diào)用順序問題如果是EAP的錯,那么.NET的事件模式就是個錯誤的設(shè)計。 大牛說這句真是不負(fù)責(zé)任嘛。
4. Jeffer Richter說:“EAP的錯誤處理和系統(tǒng)的其余部分也不一致,首先,異步不會拋出。在你的事件處理方法中,必須查詢;AsyncCompletedEventArgs的Exception屬性,看它是不是null ......” 看到這句話,我突然想到:一個月前在同事的桌上看到Jeffery Zhao 在【2010第二屆.NET技術(shù)與IT管理技術(shù)大會 的一個 The Evolution of Async Programming on .NET Platform】培訓(xùn)PPT,代碼大致是這樣寫的:
class XxxCompletedEventArgs : EventArgs {
Exception Error { get; }
TResult Result { get; }
}
所以,我懷疑:Jeffer Richter認(rèn)為EAP模式在完成時的事件中,異常也結(jié)果也是這樣分開來處理的!
大家不妨回想一下,回到Jeffery Richter所說的APM模式下,我們?yōu)榱四艿玫疆惒秸{(diào)用的結(jié)果,去調(diào)用End方法, 結(jié)果呢,如果異步在處理時,有異常發(fā)生了,此時會拋出來。是的,我也同意使用這種方式來明確的告之調(diào)用者:此時沒有結(jié)果,只有異常。
我們還是再來看一下我前面一直使用的一段代碼:
void client_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e) { if( e.Error == null ) labMessage.Text = string.Format("{0} => {1}", e.UserState, e.Result); else labMessage.Text = string.Format("{0} => Error: {1}", e.UserState, e.Error.Message); }
表面上看,這段代碼確實有Jeffer Richter所說的問題:有異常不會主動拋出。
這里有必要說明一下:有異常不主動拋出,而是依賴于調(diào)用者判斷返回結(jié)果的設(shè)計方式,是不符合.NET設(shè)計規(guī)范的。
那我如果把代碼寫成下面的這樣呢?
void client_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e) { try { labMessage.Text = string.Format("{0} => {1}", e.UserState, e.Result); } catch( Exception ex ) { labMessage.Text = string.Format("{0} => Error: {1}", e.UserState, ex.Message); } }
什么,您不認(rèn)為我直接訪問e.Result,會出現(xiàn)異常嗎?
再來看一下我寫的事件參數(shù)類型吧,看看我是如何做的:
public class CallCompletedEventArgs : AsyncCompletedEventArgs { private TOut _result; public CallCompletedEventArgs(TOut result, Exception e, bool canceled, object state) : base(e, canceled, state) { _result = result; } public TOut Result { get { base.RaiseExceptionIfNecessary(); return _result; } } }
其中,RaiseExceptionIfNecessary()方法的實現(xiàn)如下(微軟實現(xiàn)的):
protected void RaiseExceptionIfNecessary() { if( this.Error != null ) { throw new TargetInvocationException(SR.GetString("Async_ExceptionOccurred"), this.Error); } if( this.Cancelled ) { throw new InvalidOperationException(SR.GetString("Async_OperationCancelled")); } }
讓我們再來看前面的EAP模式中完成事件中的標(biāo)準(zhǔn)處理代碼:
void client_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e) { if( e.Error == null ) labMessage.Text = string.Format("{0} => {1}", e.UserState, e.Result); else labMessage.Text = string.Format("{0} => Error: {1}", e.UserState, e.Error.Message); }
的確,這種做法對于EAP模式來說:是標(biāo)準(zhǔn)的處理方式:首先要判斷this.Error != null ,為什么這個 不規(guī)范 的方式會成為標(biāo)準(zhǔn)呢?
我要再問一句:為什么不用try.....catch這種更規(guī)范的處理方式呢?
顯然,我也演示了:EAP模式在獲取結(jié)果時,也可以支持try.....catch這種方式的。在這里不用它的理由是因為:
相對于if判斷這類簡單的操作來說,拋異常是個【昂貴】的操作。這種明顯可以提高性能的做法,難道有錯嗎?
在.net設(shè)計規(guī)范中,還有Tester-Doer, Try-Parse這二類模式。我想很多人也應(yīng)該用過的吧,設(shè)計它們也是因為性能問題,與EAP的理由是一樣的。
再來總結(jié)一下。我的CallCompletedEventArgs類在實現(xiàn)時,有二個關(guān)鍵點:
1. 事件類型要從AsyncCompletedEventArgs繼承。
2. 用只讀屬性返回結(jié)果,但在訪問前,要調(diào)用基類的base.RaiseExceptionIfNecessary();
這些都是EAP模式中,正確的設(shè)計方式。什么是模式?這就是模式。什么是規(guī)范?這就是規(guī)范!
我們不能因為錯誤的設(shè)計,或者說,不尊守規(guī)范的設(shè)計,而造成的缺陷也要怪罪于EAP 。
結(jié)束語
異步是個很有用的技術(shù),不管是對于桌面程序還是服務(wù)程序都是很用意義的。
不過,相對于同步調(diào)用來說,異步也是復(fù)雜的,但它的各種使用方式也是很精彩的。
異步很精彩,故事沒講完,請繼續(xù)關(guān)注我的后續(xù)博客。
Fish Li (李奇峰)
浙公網(wǎng)安備 33010602011771號