細說ASP.NET的各類異步操做html
閱讀目錄git
在上篇博客【C#客戶端的異步操做】,我介紹了一些.net中實現異步操做的方法,在那篇博客中,我是站在整個.net平臺的角度來說述各類異步操做的實現方式,並針對各類異步操做以及不一樣的編程模型給出了一些參考建議。上篇博客談到的內容能夠算是異步操做的基礎,今天我再來談異步,專門來談在ASP.NET平臺下的各類異步操做。在這篇博客中,我主要演示在ASP.NET中如何使用各類異步操做。
在後續博客中,我還會分析ASP.NET的源碼,解釋爲何能夠這樣作,或者這樣的緣由是什麼,以解密內幕的方式向您解釋這些操做的實現原理。web
因爲本文是【C#客戶端的異步操做】的續集,所以一些關於異步的基礎內容,就再也不過多解釋了。如不理解本文的示例代碼,請先看完那篇博文吧。sql
在【C#客戶端的異步操做】的結尾,有一個小節【在Asp.net中使用異步】,我把我上次寫好的示例作了個簡單的介紹,今天我來專門解釋那些示例代碼。不過,在寫博客的過程當中,又作了一點補充,因此,請之前下載過示例代碼的朋友,大家須要從新下載那些示例代碼(仍是那篇博客中)。
說明:那些代碼都是在示範使用異步的方式調用【用Asp.net寫本身的服務框架】博客中所談到的那個服務框架,且服務方法的代碼爲:數據庫
[MyServiceMethod] express
public static string ExtractNumber(string str) 編程
{ 設計模式
// 延遲3秒,模擬一個長時間的調用操做,便於客戶演示異步的效果。瀏覽器
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#客戶端的異步操做】中提到一個觀點: 對於服務程序而言,異步處理能夠提升吞吐量。什麼是服務程序,簡單說來就是:能夠響應來自網絡請求的服務端程序。咱們熟悉的ASP.NET顯然是符合這個定義的。所以在ASP.NET程序中,適當地使用異步是能夠提升服務端吞吐量的。這裏所說的適當地使用異步,通常是說:當服務器的壓力不大且不少處理請求的執行過程被阻塞在各類I/O等待(以網絡調用爲主)操做上時,而採用異步來減小阻塞工做線程的一種替代同步調用的方法。反之,若是服務器的壓力已經足夠大,或者沒有發生各類I/O等待,那麼,在此狀況下使用異步是沒有意義的。
在.net中,幾乎全部的服務編程模型都是採用線程池處理請求任務的多線程工做模式。天然地,ASP.NET也不例外,根據【C#客戶端的異步操做】的分析,咱們就不能再使用一些將阻塞操做交給線程池的方法了。好比:委託的異步調用,直接使用線程池,都是不可取的。直接建立線程也是不合適的,所以那種方式會隨着處理請求的數量增大而建立一大堆線程,最後也將會影響性能。所以,最終能被選用的只用BeginXxxxx/EndXxxxx方式。不過,我要補充的是:還有基於事件通知的異步模式也是一個不錯的選擇(我會用代碼來證實),只要它是對原始BeginXxxxx/EndXxxxx方式的包裝。
在【用Asp.net寫本身的服務框架】中,我說過,ASP.NET處理請求是採用了一種被稱爲【管線】的方式,管線由HttpApplication控制並引起的一系列事件,由HttpHandler來處理請求,而HttpModule則更多地是一種輔助角色。還記得我在【C#客戶端的異步操做】 總結的異步特點嗎:【一路異步到底】。 ASP.NET的處理過程要通過它們的處理,天然它們對於請求的處理也必需要支持異步。幸運地是,這些負責請求處理的對象都是支持異步的。今天的博客也將着重介紹它們的異步工做方式。
WebForm框架,作爲ASP.NET平臺上最主要且默認的開發框架,我天然也會全面地介紹它所支持的各類異步方式。
MVC框架從2.0開始,也開始支持異步,本文也會介紹如何在這個版本中使用異步。
該選哪一個先出場呢?我想了好久,最後仍是決定先請出處理請求的核心對象:HttpHandler 。
異步 HttpHandler
關於HttpHandler的接口,我在【用Asp.net寫本身的服務框架】中已有介紹,這裏就再也不貼出它的接口代碼了,只想說一句:那是個同步調用接口,它並無異步功能。要想支持異步,則必須使用另外一個接口:IHttpAsyncHandler
// 摘要:
// 定義 HTTP 異步處理程序對象必須實現的協定。
public interface IHttpAsyncHandler : IHttpHandler
{
// 摘要:
// 啓動對 HTTP 處理程序的異步調用。
//
// 參數:
// context:
// 一個 System.Web.HttpContext 對象,該對象提供對用於向 HTTP 請求提供服務的內部服務器對象(如 Request、Response、Session
// 和 Server)的引用。
//
// extraData:
// 處理該請求所需的全部額外數據。
//
// cb:
// 異步方法調用完成時要調用的 System.AsyncCallback。若是 cb 爲 null,則不調用委託。
//
// 返回結果:
// 包含有關進程狀態信息的 System.IAsyncResult。
IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData);
//
// 摘要:
// 進程結束時提供異步處理 End 方法。
//
// 參數:
// result:
// 包含有關進程狀態信息的 System.IAsyncResult。
void EndProcessRequest(IAsyncResult result);
}
這個接口也很簡單,只有二個方法,而且與【C#客戶端的異步操做】 提到的BeginXxxxx/EndXxxxx設計方式差很少。若是這樣想,那麼後面的事件就好理解了。
在.net中,異步都是創建在IAsyncResult接口之上的,而BeginXxxxx/EndXxxxx是對這個接口最直接的使用方式。
下面咱們來看一下如何建立一個支持異步的ashx文件(注意:代碼中的註釋很重要)。
public class AsyncHandler : IHttpAsyncHandler {
private static readonly string ServiceUrl = "http://localhost:22132/service/DemoService/CheckUserLogin";
public void ProcessRequest(HttpContext context)
{
// 注意:這個方法沒有必要實現。由於根本就不調用它。
// 但要保留它,由於這個方法也是接口的一部分。
throw new NotImplementedException();
}
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
// 說明:
// 參數cb是一個ASP.NET的內部委託,EndProcessRequest方法將在那個委託內部被調用。
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;
// ================== 開始異步調用 ============================
// 注意:您所須要的回調委託,ASP.NET已經爲您準備好了,直接用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 {
// ============== 結束異步調用,並取得結果 ==================
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);
}
}
實現實際上是比較簡單的,大體能夠總結以下:
1. 在BeginProcessRequest()方法,調用要你要調用的異步開始方法,一般會是另外一個BeginXxxxx方法。
2. 在EndProcessRequest()方法,調用要你要調用的異步結束方法,一般會是另外一個EndXxxxx方法。
真的就是這麼簡單。
這裏要說明一下,在【C#客戶端的異步操做】中,我演示瞭如何使用.net framework中的API去實現完整的異步發送HTTP請求的調用過程,但那個過程須要二次異步,而這個IHttpAsyncHandler接口卻只支持一次回調。所以,對於這種狀況,就須要咱們本身封裝,將屢次異步轉變成一次異步。如下是我包裝的一次異步的簡化版本:
下面這個包裝類很是有用,我後面的示例還將會使用它。它也示範瞭如何建立本身的IAsyncResult封裝。所以建議仔細閱讀它。(注意:代碼中的註釋很重要)
/// <summary>
/// 對異步發送HTTP請求全過程的包裝類,
/// 按IAsyncResult接口要求提供BeginSendHttpRequest/EndSendHttpRequest方法(一次回調)
/// </summary>
/// <typeparam name="TIn"></typeparam>
/// <typeparam name="TOut"></typeparam>
public class MyHttpClient<TIn, TOut>
{
/// <summary>
/// 用於保存額外的用戶數據。
/// </summary>
public object UserData;
public IAsyncResult BeginSendHttpRequest(string url, TIn input, AsyncCallback cb, object state)
{
// 準備返回值
MyHttpAsyncResult ar = new MyHttpAsyncResult(cb, state);
// 開始異步調用
HttpWebRequestHelper<TIn, TOut>.SendHttpRequestAsync(url, input, SendHttpRequestCallback, ar);
return ar;
}
private void SendHttpRequestCallback(TIn input, TOut result, Exception ex, object state)
{
// 進入這個方法表示異步調用已完成
MyHttpAsyncResult ar = (MyHttpAsyncResult)state;
// 設置完成狀態,併發出完成通知。
ar.SetCompleted(ex, result);
}
public TOut EndSendHttpRequest(IAsyncResult ar)
{
if( ar == null )
throw new ArgumentNullException("ar");
// 說明:我並無檢查ar對象是否是與之匹配的BeginSendHttpRequest實例方法返回的,
// 雖然這是不規範的,但我仍是但願示例代碼能更簡單。
// 我想應該極少有人會亂傳遞這個參數。
MyHttpAsyncResult myResult = ar as MyHttpAsyncResult;
if( myResult == null )
throw new ArgumentException("無效的IAsyncResult參數,類型不是MyHttpAsyncResult。");
if( myResult.EndCalled )
throw new InvalidOperationException("不能重複調用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 ) // 注意這裏的延遲建立模式。
_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() 的調用者
if( _asyncCallback != null )
_asyncCallback(this); // 調用 BeginSendHttpRequest()指定的回調委託
}
internal void WaitForCompletion()
{
if( _isCompleted == false ) {
WaitHandle waitEvent = this.AsyncWaitHandle;
if( waitEvent != null )
waitEvent.WaitOne(); // 使用者直接(非回調方式)調用了EndSendHttpRequest()方法。
}
if( _exception != null )
throw _exception; // 將異步調用階段捕獲的異常從新拋出。
}
// 注意有二種線程競爭狀況:
// 1. 在回調線程中調用SetCompleted時,原線程訪問AsyncWaitHandle
// 2. 在回調線程中調用SetCompleted時,原線程調用WaitForCompletion
// 說明:在回調線程中,會先調用SetCompleted,再調用WaitForCompletion
}
對於這個包裝類來講,最關鍵仍是MyHttpAsyncResult的實現,它是異步模式的核心。
ASP.NET 異步頁的實現方式
從上面的異步HttpHandler能夠看到,一個處理流程被分紅二個階段了。但Page也是一個HttpHandler,不過,Page在處理請求時,有着更復雜的過程,一般被人們稱爲【頁面生命週期】,一個頁面生命週期對應着一個ASPX頁的處理過程。對於同步頁來講,整個過程從頭至尾連續執行一遍就好了,這比較容易理解。可是對於異步頁來講,它必需要拆分紅二個階段,如下圖片反映了異步頁的頁面生命週期。注意右邊的流程是表明異步頁的。
這個圖片是我從網上找的。原圖比較小,字體較模糊,我將原圖放大後又作了一番處理。本想在圖片中再加點說明,考慮到尊重原圖做者,沒有在圖片上加上任何多餘字符。下面我仍是用文字來補充說明一下吧。
在上面的左側部分是一個同步頁的處理過程,右側爲一個異步頁的處理過程。
這裏尤爲要注意的是那二個紅色塊的步驟:它們雖然只有一個Begin與End的操做,但它們反映的是:在一個異步頁的【頁面生命週期】中,全部異步任務在執行時所處的階段。 與HttpHandler不一樣,一個異步頁能夠發起多個異步調用任務。或許用全部這個詞也不太恰當,您就先理解爲全部吧,後面會有詳細的解釋。
引入這個圖片只是爲了能讓您對於異步頁的執行過程有個大體的印象:它將原來一個線程連續執行的過程分紅以PreRender和PreRenderComplete爲邊界的二段過程,且可能會由二個不一樣的線程來分別處理它們。請記住這個邊界,下面在演示範例時我會再次提到它們。
異步頁這個詞我已說過屢次了,什麼樣的頁面是一個異步頁呢?
簡單說來,異步頁並不要求您要實現什麼接口,只要在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
沒有任何特殊的,就是一個普通的頁面類。是的,但它已是一個異步頁了。有了這個基礎,咱們就能夠爲它添加異步功能了。
因爲ASP.NET的異步頁有 3 種實現方式,我也將分別介紹它們。請繼續往下閱讀。
1. 調用Page.AddOnPreRenderCompleteAsync()的異步頁
在.net的世界裏,許多支持異步的原始API都採用了Begin/End的設計方式,都是基於IAsyncResult接口的。爲了能方便地使用這些API,ASP.NET爲它們設計了正好匹配的調用方式,那就是直接調用Page.AddOnPreRenderCompleteAsync()方法。這個方法的名字也大概說明它的功能:添加一個異步操做到PreRenderComplete事件前。咱們仍是來看一下這個方法的簽名吧:
// 摘要:
// 爲異步頁註冊開始和結束事件處理程序委託。
//
// 參數:
// state:
// 一個包含事件處理程序的狀態信息的對象。
//
// endHandler:
// System.Web.EndEventHandler 方法的委託。
//
// beginHandler:
// System.Web.BeginEventHandler 方法的委託。
//
// 異常:
// System.InvalidOperationException:
// <async> 頁指令沒有設置爲 true。- 或 -System.Web.UI.Page.AddOnPreRenderCompleteAsync(System.Web.BeginEventHandler,System.Web.EndEventHandler)
// 方法在 System.Web.UI.Control.PreRender 事件以後調用。
//
// 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的定義以下:
// 摘要:
// 表示處理異步事件(如應用程序事件)的方法。此委託在異步操做開始時調用。
//
// 返回結果:
// System.IAsyncResult,它表示 System.Web.BeginEventHandler 操做的結果。
public delegate IAsyncResult BeginEventHandler(object sender, EventArgs e, AsyncCallback cb, object extraData);
// 摘要:
// 表示處理異步事件(如應用程序事件)的方法。
public delegate void EndEventHandler(IAsyncResult ar);
若是單看以上接口的定義,能夠發現除了"object sender, EventArgs e"是多餘部分以外,其他部分則恰好與Begin/End的設計方式徹底吻合,沒有一點多餘。
咱們來看一下如何調用這個方法來實現異步的操做:(注意代碼中的註釋)
protected void button1_click(object sender, EventArgs e)
{
Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
// 準備回調數據,它將由AddOnPreRenderCompleteAsync的第三個參數被傳入。
MyHttpClient<string, string> http = new MyHttpClient<string, string>();
http.UserData = textbox1.Text;
// 註冊一個異步任務。注意這三個參數哦。
AddOnPreRenderCompleteAsync(BeginCall, EndCall, http);
}
private IAsyncResult BeginCall(object sender, EventArgs e, AsyncCallback cb, object extraData)
{
// 在這個方法中,
// sender 就是 this
// e 就是 EventArgs.Empty
// cb 就是 EndCall
// extraData 就是調用AddOnPreRenderCompleteAsync的第三個參數
Trace.Write("BeginCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
MyHttpClient<string, string> http = (MyHttpClient<string, string>)extraData;
// 開始一個異步調用。頁面線程也最終在執行這個調用後返回線程池了。
// 中間則是等待網絡的I/O的完成通知。
// 若是網絡調用完成,則會調用 cb 對應的回調委託,其實就是下面的方法
return http.BeginSendHttpRequest(ServiceUrl, (string)http.UserData, cb, http);
}
private void EndCall(IAsyncResult ar)
{
// 到這個方法中,表示一個任務執行完畢。
// 參數 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{
// 結束異步調用,獲取調用結果。若是有異常,也會在這裏拋出。
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中的介紹,你會發現它們很是像。
若是要執行多個異步任務,能夠參考下面的代碼:
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);
}
也很簡單,就是調用二次AddOnPreRenderCompleteAsync而已。
前面我說過,異步的處理是發生在PreRender和PreRenderComplete之間,咱們來仍是看一下究竟是不是這樣的。在ASP.NET的Page中,咱們很容易的輸出一些調試信息,且它們會顯示在所處的頁面生命週期的相應執行階段中。這個方法很簡單,在Page指令中加上【Trace="true"】選項,並在頁面類的代碼中調用Trace.Write()或者Trace.Warn()就能夠了。下面來看一下我加上調試信息的頁面執行過程吧。
從這張圖片中,咱們至少能夠看到二個信息:
1. 全部的異步任務的執行過程確實發生在PreRender和PreRenderComplete之間。
2. 全部的異步任務被串行地執行了。
2. 調用Page.RegisterAsyncTask()的異步頁
我一直認爲ASP.NET程序也是一種服務程序,它要對客戶端瀏覽器發出的請求而服務。因爲是服務,對於要服務的對象來講,都但願能儘快地獲得響應,這其實也是對服務的一個基本的要求,那就是:高吞量地快速響應。
對於前面所說的方法,顯然,它的全部異步任務都是串行執行的,對於客戶端來講,等待的時間會較長。並且,最嚴重的是,若是服務超時,上面的方法會一直等待,直到本次請求超時。爲了解決這二個問題,ASP.NET定義了一種異步任務類型:PageAsyncTask 。它能夠解決以上二種問題。首先咱們仍是來看一下PageAsyncTask類的定義:(說明:這個類的關鍵就是它的構造函數)
// 摘要:
// 使用並行執行的指定值初始化 System.Web.UI.PageAsyncTask 類的新實例。
//
// 參數:
// state:
// 表示任務狀態的對象。
//
// executeInParallel:
// 指示任務可否與其餘任務並行處理的值。
//
// endHandler:
// 當任務在超時期內成功完成時要調用的處理程序。
//
// timeoutHandler:
// 當任務未在超時期內成功完成時要調用的處理程序。
//
// beginHandler:
// 當異步任務開始時要調用的處理程序。
//
// 異常:
// System.ArgumentNullException:
// beginHandler 參數或 endHandler 參數未指定。
public PageAsyncTask(BeginEventHandler beginHandler, EndEventHandler endHandler,
EndEventHandler timeoutHandler, object state, bool executeInParallel);
注意這個構造函數的簽名,它與AddOnPreRenderCompleteAsync()相比,多了二個參數:EndEventHandler timeoutHandler, bool executeInParallel 。它們的含義上面的註釋中有說明,這裏只是提示您要注意它們而已。
建立好一個PageAsyncTask對象後,只要調用頁面的RegisterAsyncTask()方法就能夠註冊一個異步任務。具體用法可參考個人以下代碼:(注意代碼中的註釋)
protected void button1_click(object sender, EventArgs e)
{
Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
// 準備回調數據,它將由PageAsyncTask構造函數的第四個參數被傳入。
MyHttpClient<string, string> http = new MyHttpClient<string, string>();
http.UserData = textbox1.Text;
// 建立異步任務
PageAsyncTask task = new PageAsyncTask(BeginCall, EndCall, TimeoutCall, http);
// 註冊異步任務
RegisterAsyncTask(task);
}
private IAsyncResult BeginCall(object sender, EventArgs e, AsyncCallback cb, object extraData)
{
// 在這個方法中,
// sender 就是 this
// e 就是 EventArgs.Empty
// cb 是ASP.NET定義的一個委託,咱們只管在異步調用它時把它用做回調委託就好了。
// extraData 就是PageAsyncTask構造函數的第四個參數
Trace.Warn("BeginCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
MyHttpClient<string, string> http = (MyHttpClient<string, string>)extraData;
// 開始一個異步調用。
return http.BeginSendHttpRequest(ServiceUrl, (string)http.UserData, cb, http);
}
private void EndCall(IAsyncResult ar)
{
// 到這個方法中,表示一個任務執行完畢。
// 參數 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 {
// 結束異步調用,獲取調用結果。若是有異常,也會在這裏拋出。
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)
{
// 到這個方法,就表示任務執行超時了。
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是支持超時的,那麼它的超時功能是如何使用的呢,上面的示例只是給了一個超時的回調委託而已。
在開始演示PageAsyncTask的高級功能前,有必要說明一下示例所調用的服務端代碼。本示例所調用的服務是【C#客戶端的異步操做】中使用的演示服務,服務代碼以下:
[MyServiceMethod]
public static string ExtractNumber(string str)
{
// 延遲3秒,模擬一個長時間的調用操做,便於客戶演示異步的效果。
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());
}
下面的示例我將演示開始二個異步任務,並設置異步頁的超時時間爲4秒鐘。
protected void button1_click(object sender, EventArgs e)
{
Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
// 設置頁面超時時間爲4秒
Page.AsyncTimeout = new TimeSpan(0, 0, 4);
// 註冊第一個異步任務
MyHttpClient<string, string> http = new MyHttpClient<string, string>();
http.UserData = textbox1.Text;
PageAsyncTask task = new PageAsyncTask(BeginCall, EndCall, TimeoutCall, http);
RegisterAsyncTask(task);
// 註冊第二個異步任務
MyHttpClient<string, string> http2 = new MyHttpClient<string, string>();
http2.UserData = "T2_" + Guid.NewGuid().ToString();
PageAsyncTask task2 = new PageAsyncTask(BeginCall2, EndCall2, TimeoutCall2, http2);
RegisterAsyncTask(task2);
}
此頁面的執行過程以下:
確實,第二個任務執行超時了。
再來看一下PageAsyncTask所支持的任務的並行執行是如何調用的:
protected void button1_click(object sender, EventArgs e)
{
Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
// 設置頁面超時時間爲4秒
Page.AsyncTimeout = new TimeSpan(0, 0, 4);
// 註冊第一個異步任務
MyHttpClient<string, string> http = new MyHttpClient<string, string>();
http.UserData = textbox1.Text;
PageAsyncTask task = new PageAsyncTask(BeginCall, EndCall, TimeoutCall, http, true /*注意這個參數*/);
RegisterAsyncTask(task);
// 註冊第二個異步任務
MyHttpClient<string, string> http2 = new MyHttpClient<string, string>();
http2.UserData = "T2_" + Guid.NewGuid().ToString();
PageAsyncTask task2 = new PageAsyncTask(BeginCall2, EndCall2, TimeoutCall2, http2, true /*注意這個參數*/);
RegisterAsyncTask(task2);
}
此頁面的執行過程以下:
圖片清楚地反映出,這二個任務是並行執行時,因此,這二個任務能在4秒內同時執行完畢。
在結束對PageAsyncTask的介紹前,有必要對超時作個說明。對於使用PageAsyncTask的異步頁來講,有二種方法來設置超時時間:
1. 經過Page指令: asyncTimeout="0:00:45" ,這個值就是異步頁的默認值。至於這個值的含義,我想您應該懂的。
2. 經過設置 Page.AsyncTimeout = new TimeSpan(0, 0, 4); 這種方式。示例代碼就是這種方式。
注意:因爲AsyncTimeout是Page級別的參數,所以,它是針對全部的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的異步頁中的實現代碼以下:
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);
}
搞什麼呀,這二段代碼是同樣的嘛。 您是否是也有這樣的感受呢?
仔細看這二段代碼,仍是能發現它們有區別的。這裏我就不指出它們了。它們與異步無關,說出它們意義不大,反而,我更但願您對【基於事件模式的異步】留個好印象:它們就是同樣的。
再來看一下如何發出多個異步任務:
protected void button1_click(object sender, EventArgs e)
{
Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
string str = textbox1.Text;
// 注意:這個異步任務,我設置了2秒的超時。它應該是不能按時完成任務的。
MyAysncClient<string, string> client = new MyAysncClient<string, string>(ServiceUrl, 2000);
client.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client_OnCallCompleted);
client.CallAysnc(str, str); // 開始第一個異步任務
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); // 開始第二個異步任務
}
void client2_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e)
{
ShowCallResult(2, e);
// 再來一個異步調用
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); // 開始第三個異步任務
}
頁面的執行過程以下圖:
這裏要說明一下了:在【C#客戶端的異步操做】中我就給出這個類的實現代碼,不過,此次我給它增長了超時功能,增長了一個重載的構造函數,須要在構造函數的第二個參數傳入。今天我就不貼出那個類的代碼了,有興趣的本身去下載代碼閱讀吧。在上次貼的代碼,你應該能夠發現,在CallAysnc()時,就已經開始了異步操做。對於本示例來講,也就是在button1_click就已經開始了二個異步操做。
這是個什麼意思呢?
能夠這樣來理解:前二個任務顯然是和LoadComplete,PreRender事件階段的代碼在並行執行的。
有意思的是:第三個任務是在第二個任務的結束事件中開始的,但三個任務的結束操做全在頁面的PreRender事件才獲得處理。下面我再把這個例子來改一下,就更有趣了:
protected void button1_click(object sender, EventArgs e)
{
Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
string str = textbox1.Text;
// 注意:這個異步任務,我設置了2秒的超時。它應該是不能按時完成任務的。
MyAysncClient<string, string> client = new MyAysncClient<string, string>(ServiceUrl, 2000);
client.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client_OnCallCompleted);
client.CallAysnc(str, str); // 開始第一個異步任務
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); // 開始第二個異步任務
}
如今,在第一個任務發出後,我讓線程等待了3秒,也就是等到了第一個任務的超時。而後再開始第二個任務。
也就是說:在button1_click事件還沒執行完畢,第一個任務就結束了。
如今,您能夠猜一下,此時的執行過程是個什麼樣的。
猜好了就來看下圖吧。
如今明白了吧:哪怕是在PostBackEvent階段就結束的任務,也要等到PreRender以後才能獲得處理。
至於爲何會是這樣的,我之後再講。今天只要記住本文的第一張圖片就行了。
我但是好不容易纔找出這張圖片來的,且爲了讓您能看得更清楚,還花了些時間修改了它。
在那個圖片後面我還說過:在一個異步頁的【頁面生命週期】中,全部異步任務在執行時所處的階段。 並在後面註明了這裏的全部這個詞也不太恰當。如今能夠解釋爲何不恰當了:
【基於事件模式的異步】的開始階段並不必定要PreRender事件以後,而對於前二種異步面的實現方式則是確定在PreRender事件以後。
至於這其中的緣由,一樣,您要等待個人後續博客了。
各類異步頁的實現方式比較
前面介紹了3種異步頁的實現方式,我打算在這裏給它們作個總結及比較。固然,這一切只表明我我的的觀點,僅供參考。
爲了能給出一個客觀的評價,我認爲先有必要再給個示例,把這些異步方式放在一塊兒執行,就好像把它們放在一塊兒比賽同樣,或許這樣會更有意思,同時也會讓我給出的評價更有說服力。
在下面的示例中,我把上面說過的3種異步方式放在一塊兒,並讓每種方法執行屢次(共10個異步任務),實驗代碼以下:
protected void button1_click(object sender, EventArgs e)
{
ShowThreadInfo("button1_click");
// 爲PageAsyncTask設置超時時間
Page.AsyncTimeout = new TimeSpan(0, 0, 7);
// 開啓4個PageAsyncTask,其中第1,4個任務不接受並行執行,2,3則容許並行執行
Async_RegisterAsyncTask("RegisterAsyncTask_1", false);
Async_RegisterAsyncTask("RegisterAsyncTask_2", true);
Async_RegisterAsyncTask("RegisterAsyncTask_3", true);
Async_RegisterAsyncTask("RegisterAsyncTask_4", false);
// 開啓3個AddOnPreRenderCompleteAsync的任務
Async_AddOnPreRenderCompleteAsync("AddOnPreRenderCompleteAsync_1");
Async_AddOnPreRenderCompleteAsync("AddOnPreRenderCompleteAsync_2");
Async_AddOnPreRenderCompleteAsync("AddOnPreRenderCompleteAsync_3");
// 最後開啓3個基於事件通知的異步任務,其中第2個任務因爲設置了超時,將不能成功完成。
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);
}
執行過程以下圖:
不知您看到這個執行過程是否會想到爲何會是這個樣子的。至於爲何會是這個樣子的,這就涉及到ASP.NET的異步頁的執行過程,這個過程比較複雜,我之後再談。今天我們就來根據這個圖片來談談比較表面化的東西,談一下這三種方式的差異。
從上面的代碼以及執行過程,能夠看到一個有趣的現象,我明明是先註冊的4個PageAsyncTask 。但是呢,最早顯示的倒是【BeginCall AddOnPreRenderCompleteAsync_1】。我想我這裏使用顯示這個詞也是比較恰當的,爲何呢?由於,我前面已經解釋過了,基於事件的異步的任務應該是在button1_click事件處理器中先執行的,只是我沒有讓它們顯示罷了。接下來的故事也很天然,因爲我將"MyAysncClient_2"設置爲2秒的超時,它最早完成,只是結果爲超時罷了。緊接着,"MyAysncClient_1"和"MyAysncClient_3"也執行結束了。嗯,是的:3個事件的異步任務全執行完了。
說到這裏我要另起一段了,以提醒您的注意。
有沒有注意到,前面說到的3個事件的異步任務全執行完了。這個時候,其它的異步任務絕大部分尚未開始呢,它們3個咋就先執行完了呢?
有意思吧,其實何止3個,若是再來5個基於事件的異步任務,它們仍是會先執行完成,不信的話,看下圖:
或許舉這個例子把基於事件的異步方式捧高了。這裏我也要客觀的解釋一下緣由了:
出現這個現象主要由2個緣由形成的:
1. 在這個例子中,"MyAysncClient_1", "MyAysncClient_2", "MyAysncClient_3", "AddOnPreRenderCompleteAsync_1" 因爲都是異步任務,因此基本上是並行執行的,
2. 因爲3個基於事件的異步方式先執行的,所以它們先結束了。
接着來解釋圖片所反映的現象。當基於事件的異步任務全執行完成後," EndCall AddOnPreRenderCompleteAsync_1" 也被調用了。說明"AddOnPreRenderCompleteAsync_1"這個任務完全地執行完了。接下來,"AddOnPreRenderCompleteAsync_2","AddOnPreRenderCompleteAsync_3"也依次執行完了。
我一開始用RegisterAsyncTask註冊的4個異步任務呢?終於,在前面的全部異步任務所有執行完成後,纔開始了這類任務的執行過程。首先執行的是"RegisterAsyncTask_1",這個好理解。接下來,"BeginCall RegisterAsyncTask_2", "BeginCall RegisterAsyncTask_3"被連續調用了,這也好理解吧,由於我當時建立異步任務時,指定它們是容許與其它任務並行執行的,所以它們是一塊兒執行的。 3秒後,2個任務同時執行完了,最後啓動了"RegisterAsyncTask_4",因爲它不支持並行執行,因此,它排在最後,在沒有任何懸念中,"TimeoutCall RegisterAsyncTask_4"被調用了。這麼正常啊,我設置過Page.AsyncTimeout = new TimeSpan(0, 0, 7); 所以,前二批PageAsyncTask趕在超時前正常結束了,留給"RegisterAsyncTask_4"的執行時間只有1秒,它固然就不能在指定時間內正常完成。
彷佛到這裏,這些異步任務的執行過程都解釋完了,可是,有二個很奇怪的現象您有沒有發現:
1. 爲何AddOnPreRenderCompleteAsync的任務全執行完了以後,才輪到PageAsyncTask的任務呢?
2. 還有前面說過的,爲何是"BeginCall AddOnPreRenderCompleteAsync_1"最早顯示呢?
這一切絕非偶然,若是您有興趣,可下載個人示例代碼,你運行千遍萬遍還將是這個結果。
這些緣由我之後再談,今天的博客只是想告訴您這樣一個結果就好了。
不過,爲了能讓您能容易地理解後面的內容,我暫且告訴您:PageAsyncTask是創建在AddOnPreRenderCompleteAsync的基礎上的。
有了前面這些實驗結果,咱們再來對這3種異步頁方法作個總結及比較。
1. AddOnPreRenderCompleteAsync: 它提供了最基本的異步頁的使用方法。就好像HttpHandler同樣,它雖能處理請求,但不太方便,顯得比較原始。因爲它提供的是比較原始的方法,您也能夠自行包裝您的高級功能。
2. PageAsyncTask: 與AddOnPreRenderCompleteAsync相比,它增長了超時以及並行執行的功能,但我也說過,它是創建在AddOnPreRenderCompleteAsync的基礎之上的。若是把AddOnPreRenderCompleteAsync比做爲HttpHandler,那麼PageAsyncTask則就像是Page 。所以它只是作了些高級的包裝罷了。
3. 基於事件的異步方式:與前2者徹底沒有關係,它只依賴於AspNetSynchronizationContext。這裏有必要強調一下:【基於事件的異步方式】能夠理解爲一個設計模式,也能夠把它理解成對最基礎的異步方式的高級包裝。它能提供或者完成的功能,依賴於包裝的方式及力度。在我提供的這個包裝類中,它也能夠實現與PageAsyncTask同樣的並行執行以及超時功能。
後二種方法功能強大的緣由是來源於高級包裝,因爲包裝,過程也會更復雜,所以性能或許也會有微小的損失。若是您不能接受這點性能損失,可能仍是選AddOnPreRenderCompleteAsync會比較合適。不過,我要再次提醒您:它不支持並行執行,不支持超時。
請容忍我再誇一下【基於事件的異步模式】,從我前面的示例代碼,尤爲是與WinForm中的示例代碼的比較中,咱們能夠清楚的發現,這種方式是很是易用的。掌握了這種方式,至少在這二大編程模型中都是適用的。並且,它能在異步頁的執行週期中,較早的進入異步等待狀態,所以能更快的結束執行過程。想一想【從"Begin Raise PostBackEvent"到"End PreRender"這中間還能夠執行多少代碼是不肯定的】吧。
【基於事件的異步模式】的優勢不只如此,個人演示代碼中還演示了另外一種用法: 在一個完成事件中,我還能再開啓另外一個異步任務。 這個優勢使我能夠有選擇性地啓動後續的異步操做。可是,這個特性是另2個不可能作到的!這個緣由能夠簡單地表達爲:在PreRender事件後,調用AddOnPreRenderCompleteAsync會拋異常。
異步HttpModule的實現方式
在【用Asp.net寫本身的服務框架】中,我示範過若是編寫一個HttpModule,一般只要咱們實現IHttpModule接口,並在Init方法中訂閱一些事件就能夠了:
internal class DirectProcessRequestMoudle : IHttpModule
{
public void Init(HttpApplication app)
{
app.PostAuthorizeRequest += new EventHandler(app_PostAuthorizeRequest);
}
HttpHandler有異步接口的IHttpAsyncHandler,但HttpModule卻只有一個接口:IHttpModule,無論是同步仍是異步。異步HttpModule的實現方式並非訂閱HttpApplication的事件,而是調用HttpApplication的一些註冊異步操做的方法來實現的(仍是在Init事件中),這些方法可參考如下列表:
// 將指定的 System.Web.HttpApplication.AcquireRequestState 事件
// 添加到當前請求的異步 System.Web.HttpApplication.AcquireRequestState事件處理程序的集合。
public void AddOnAcquireRequestStateAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.AuthenticateRequest 事件
// 添加到當前請求的異步 System.Web.HttpApplication.AuthenticateRequest事件處理程序的集合。
public void AddOnAuthenticateRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.AuthorizeRequest 事件
// 添加到當前請求的異步 System.Web.HttpApplication.AuthorizeRequest事件處理程序的集合。
public void AddOnAuthorizeRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.BeginRequest 事件
// 添加到當前請求的異步 System.Web.HttpApplication.BeginRequest事件處理程序的集合。
public void AddOnBeginRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.EndRequest 事件
// 添加到當前請求的異步 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 事件
// 添加到當前請求的異步 System.Web.HttpApplication.PostAcquireRequestState事件處理程序的集合。
public void AddOnPostAcquireRequestStateAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.PostAuthenticateRequest 事件
// 添加到當前請求的異步 System.Web.HttpApplication.PostAuthenticateRequest事件處理程序的集合。
public void AddOnPostAuthenticateRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.PostAuthorizeRequest 事件
// 添加到當前請求的異步 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 事件
// 添加到當前請求的異步 System.Web.HttpApplication.PostMapRequestHandler事件處理程序的集合。
public void AddOnPostMapRequestHandlerAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.PostReleaseRequestState 事件
// 添加到當前請求的異步 System.Web.HttpApplication.PostReleaseRequestState事件處理程序的集合。
public void AddOnPostReleaseRequestStateAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.PostRequestHandlerExecute 事件
// 添加到當前請求的異步 System.Web.HttpApplication.PostRequestHandlerExecute事件處理程序的集合。
public void AddOnPostRequestHandlerExecuteAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.PostResolveRequestCache 事件
// 添加到當前請求的異步 System.Web.HttpApplication.PostResolveRequestCache事件處理程序的集合。
public void AddOnPostResolveRequestCacheAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.PostUpdateRequestCache 事件
// 添加到當前請求的異步 System.Web.HttpApplication.PostUpdateRequestCache事件處理程序的集合。
public void AddOnPostUpdateRequestCacheAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.PreRequestHandlerExecute 事件
// 添加到當前請求的異步 System.Web.HttpApplication.PreRequestHandlerExecute事件處理程序的集合。
public void AddOnPreRequestHandlerExecuteAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.ReleaseRequestState 事件
// 添加到當前請求的異步 System.Web.HttpApplication.ReleaseRequestState事件處理程序的集合。
public void AddOnReleaseRequestStateAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.ResolveRequestCache 事件處理程序
// 添加到當前請求的異步 System.Web.HttpApplication.ResolveRequestCache事件處理程序的集合。
public void AddOnResolveRequestCacheAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 將指定的 System.Web.HttpApplication.UpdateRequestCache 事件
// 添加到當前請求的異步 System.Web.HttpApplication.UpdateRequestCache事件處理程序的集合。
public void AddOnUpdateRequestCacheAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
每一個方法的含義從它們的名字是能夠看出。異步HttpModule的實現方式須要將異步對應的Begin/End二個方法分別作爲委託參數傳入這些方法中。
注意:這些方法的簽名與Page.AddOnPreRenderCompleteAsync()是一致的,所以它們的具體用法也與Page.AddOnPreRenderCompleteAsync()同樣。
爲何這裏不設計成訂閱事件的方式?
我想是由於:若是採用事件模式,調用者能夠只訂閱其中的一個事件,ASP.NET不容易控制,還有"object state"這個參數不便於在訂閱事件時傳入。
異步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] = "數據庫列表:" + sb.ToString(0, sb.Length - 2);
}
public void Dispose()
{
}
}
頁面可使用以下方式得到MyAsyncHttpModule的結果:
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在執行時,只是在一個階段內,全部的HttpModule採用異步方式工做。當進入下一個階段前,必需要等到全部HttpModule所有在當前階段內執行完畢。
一般狀況下,是沒有必要寫異步的HttpModule的。這是我寫的第一個異步HttpModule。
異步的 Web Service
因爲Web Service也是受ASP.NET支持,且隨着ASP.NET一塊兒出現。咱們再來看一下若是將一個同步的服務方法改變成異步的方法。
注意:將方法由同步改爲異步版本,是不影響客戶端的。
如下代碼是一個同步版本的服務方法:
[WebMethod]
public string ExtractNumber(string str)
{
//return ........
}
再來看一下最終的異步實現版本:
[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的工做差很少,在原有的同步方法後面加二個與異步操做有關的參數,而且返回值改成IAsyncResult,而後再添加一個EndXxxx方法就能夠了,固然了,EndXxxx方法的傳入參數只能是一個IAsyncResult類型的參數。
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); // 開始異步調用
}
void client_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e)
{
// 告訴ASP.NET MVC,一個異步操做結束了。
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"] 用於寫輸出結果。
// 這裏仍然採用相似ViewData的設計。
// 注意:key 的名稱要和Test1Completed的參數名匹配。
}
// 注意名稱後要加上 Completed ,且其他部分與Test1Async的前綴對應。
public ActionResult Test1Completed(string result)
{
ViewData["result"] = result;
return View();
}
說明:若是您認爲單獨爲事件處理器寫個方法看起來不爽,您也能夠採用匿名委託之類的閉包寫法,這個純屬我的喜愛問題。
再來個屢次異步操做的示例:
public void Test2Async()
{
// 表示要開啓3個異步操做。
// 若是把這個數字設爲2,極有可能會產生的錯誤的結果。不信您能夠試一下。
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); // 開始第一個異步任務
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); // 開始第二個異步任務
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); // 開始第三個異步任務
}
void client2_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e)
{
// 遞減內部的異步任務累加器。有點相似AspNetSynchronizationContext的設計。
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,它看起來應該是這樣的:
public ActionResult Test1()
{
return View();
}
首先,我須要把它的返回值改爲void, 並把方法名稱修改成Test1Async 。
而後,在開始異步調用前,調用AsyncManager.OutstandingOperations.Increment();
在異步完成時:
1. 要調用AsyncManager.OutstandingOperations.Decrement();
2. 將結果寫入到AsyncManager.Parameters[]這個集合中。注意key的名字後面要用到。
到這裏,異步開發的任務算是作了一大半了。你可能會想我在哪裏返回ActionResult呢?
再來建立一個Test1Completed方法,簽名應該是這個樣子的:
public ActionResult Test1Completed(string result)
注意:方法中的參數名要和前面說過的寫AsyncManager.Parameters[]的key名一致,包括數量。
再後面的事情,我想您懂的,我就很少說了。
再來講說我對【ASP.NET MVC的異步方式】這個設計的感覺吧。
簡單說來就是:不夠完美。
要知道在這個例子中,我但是採用的基於事件的異步模式啊,在異步頁中,哪有這些額外的調用?
對於這個設計,我至少有2點不滿意:
1. AsyncManager.OutstandingOperations.Increment(); Decrement();由使用者來控制,容易出錯。
2. AsyncManager.Parameters[]這個bag設計方式也不爽,難道僅僅是爲了簡單?由於我能夠在完成事件時,根據條件繼續後面的異步任務,最終結果可能並不肯定,所以後面的XXXXCompleted方法的簽名就是個問題了。
爲何在ASP.NET MVC中,這個示例須要調用Increment(); Decrement(),而在異步頁中不須要呢?
恐怕有些人會對此有好奇,我就告訴你們吧:這與AspNetSynchronizationContext有關。
AspNetSynchronizationContext,真是個【成也蕭何,敗成蕭何】的東西,在異步頁爲何不須要咱們調用相似Increment(); Decrement()的語句是由於,它內部也有個這樣的累加器,不過,當時在設計基於事件的異步模式時,在ASP.NET運行環境中,SynchronizationContext就是使用了AspNetSynchronizationContext這個具體實現類,但它的絕大部分紅員倒是internal類型的。若是可使用它,能夠用一種簡便地方式設置一個統一的回調委託:
if( this._syncContext.PendingOperationsCount > 0 ) {
this._syncContext.SetLastCompletionWorkItem(this._callHandlersThreadpoolCallback);
}
就這麼一句話,能夠不用操心使用者到底開始了多少個異步任務,均可以在全部的異步結束後,回調指定的委託。只是惋惜的是,這二個成員都是internal的!
若是當初微軟設計AspNetSynchronizationContext時,不開放SetLastCompletionWorkItem這個方法,是擔憂使用者亂調用致使ASP.NET運行錯誤的話,如今ASP.NET MVC的這種設計顯然更容易出錯。固然了,ASP.NET MVC出來的時候,這一切早就出現了,所以它也沒法享受AspNetSynchronizationContext的便利性。不過,最讓我想不通的是:直到ASP.NET 4.0,這一切仍是原樣。難道是由於ASP.NET MVC獨立在升級,連InternalsVisibleTo的機會也不給它嗎?
就算咱們不用基於事件的異步模式,異步頁還有二種實現方法呢(都不須要累加器),但是ASP.NET MVC卻沒有實現相似的功能。因此,這樣就顯得很不完善。咱們也只能期待將來的版本能改進這些問題了。
MSDN參考文章:在 ASP.NET MVC 中使用異步控制器
受爭論的【基於事件的異步模式】
原本在個人寫做計劃中,是沒有這段文字的,可就在我打算髮布這篇博客以前,想到上篇博客中的評論,忽然我想到一本書:CLR via C# 。是的,就是這本書,我想不少人手裏有這本書,想到這本書是由於上篇博客的評論中,出現一個與個人觀點有着不一致的聲音(來自AndersTan),而他應該是Jeffer Richter的粉絲。我早就買了這本書了(中文第三版),其實也是AndersTan推薦的,不過一直沒有看完,所以,根本就沒有發現Jeffer Richter是【基於事件的異步模式】的反對者,這個可參考書中696頁。Jeffer Richter在書中說:"因爲我不是EAP的粉絲,並且我不贊同使用這個模式, 因此一直沒有花太多的時間在它上面。然而,我知道有一些人確實喜歡這個模式,並且想使用它,因此我專門花了一些時間研究它。" 爲了表示對大牛的敬重,我用藍色字體突出他說的話(固然是由周靖翻譯的)。看到這句話以及後面他對於此模式的評價,尤爲是在【27.11.2 APM和EAP的對比】這個小節中對於EAP的評價,讓我感受大牛其實也沒有很好地瞭解這個模式。
這裏再補充一下,書中提到二個英文簡寫:EAP: Event-base Asynchronous Pattern, APM: Asynchronous Programming Model 。書中689頁中,Jeffer Richter還說過:"雖然我是APM的超級粉絲,可是我必須認可它存在的一些問題。" 與之相反,雖然我不是APM的忠實粉絲,我卻不認爲他所說的問題真的是APM的缺點。他說的第一點,感受就沒有意義。我不知道有多少人在現實使用中,是在調用了Begin方法後,當即去調用End方法?我認爲.net容許這種使用方式,可能仍是更看中的是使用上的靈活性,畢竟微軟要面對的開發者會有千奇百怪的要求。並且MSDN中也解釋了這種調用會阻塞線程。訪問IAsyncResult是能夠獲得一個WaitHandle對象,這個好像在上篇博客的評論中有人也提過了,我當時也不想說了,此次就把個人實現方式貼出來了,只但願告訴一些人:這個成員雖然是個耗資源的東西,但要看你如何去實現它了:有些時候(異步完成的時候)能夠返回null的,因此,一般應該設計成一種延遲建立模式纔對(我再一次的提醒:在設計它時要考慮多線程的併發訪問)。
剛纔扯遠了,咱們仍是來講關於Jeffer Richter對於【27.11.2 APM和EAP的對比】這個小節的見解(699頁)。這個小節有4個段話,分別從4個方面說了些EAP的【缺點】,我也將依次來發表個人觀點。
1. Jeffer Richter認爲EAP的最大優勢在於和IDE的配合使用,且在後面一直提到GUI線程。顯然EAP模式被表明了,被WinForm這類桌面程序程序表明了。我今天的示例代碼所有是能夠在ASP.NET環境下運行的,並且還特地和WinForm下的使用方法作了比較,結果是:使用方式基本相同。 我認爲這個結果纔是EAP模式最大的優勢:在不一樣的編程模型中沒必要考慮線程模型問題。
2. Jeffer Richter說:事實上,EAP必須爲引起的全部進度報告和完成事件分配從EventArgs派生的對象......。看到這句話的感受仍是和上句話差很少:被表明了。 對於這段話,我認爲有必要從幾個角度來表達個人觀點:
a. 進度報告:我想問一句:ASP.NET編程模型下進度報告有什麼意義,或者說如何實現?在我今天演示的示例代碼中,我一直沒演示進度報告吧?事實上,個人包裝類中根本就不提供這個功能,只提供了完成事件的通知功能。再說,爲何須要進度報告?由於桌面程序須要,它們爲了能讓程序擁有更好的用戶體驗。固然也能夠不提供進度報告嘛,大不了讓用戶守在電腦面前傻等就是了,這樣還會有性能損失嗎?固然沒有,可是用戶可能會罵人......。
b. 性能損失:MyAysncClient是對一個更底層的靜態方法調用的封裝。我也很明白:有封裝就有性能的損失。但我想:一次異步任務也就只通知一次,性能損失能有多大?並且明知道有性能損失,我爲何還要封裝呢?只爲一個很簡單的理由:使用起來更容易!
c. 對象的回收問題:若是按照Jeffer Richter的說法,多建立這幾個對象就讓GC爲難的話,會讓我對.NET失去信心,連ASP.NET也不敢用了,由於:要知道.NET的世界是徹底面向對象的世界,一次WEB請求的處理過程當中,ASP.NET不知道要建立多少個對象,我真的數不清楚。
3. Jeffer Richter說:若是在登記事件處理方法以前調用XxxAsync方法,......。看到這裏,我笑了。顯然,大牛是很是討厭EAP模式的。EAP是使用了事件,這個錯誤的調用順序問題若是是EAP的錯,那麼.NET的事件模式就是個錯誤的設計。大牛說這句真是不負責任嘛。
4. Jeffer Richter說:"EAP的錯誤處理和系統的其他部分也不一致,首先,異步不會拋出。在你的事件處理方法中,必須查詢;AsyncCompletedEventArgs的Exception屬性,看它是否是null ......" 看到這句話,我忽然想到:一個月前在同事的桌上看到Jeffery Zhao 在【2010第二屆.NET技術與IT管理技術大會的一個 The Evolution of Async Programming on .NET Platform】培訓PPT,代碼大體是這樣寫的:
class XxxCompletedEventArgs : EventArgs {
Exception Error { get; }
TResult Result { get; }
}
因此,我懷疑:Jeffer Richter認爲EAP模式在完成時的事件中,異常也結果也是這樣分開來處理的!
你們不妨回想一下,回到Jeffery Richter所說的APM模式下,咱們爲了能獲得異步調用的結果,去調用End方法,結果呢,若是異步在處理時,有異常發生了,此時會拋出來。是的,我也贊成使用這種方式來明確的告之調用者:此時沒有結果,只有異常。
咱們仍是再來看一下我前面一直使用的一段代碼:
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所說的問題:有異常不會主動拋出。
這裏有必要說明一下:有異常不主動拋出,而是依賴於調用者判斷返回結果的設計方式,是不符合.NET設計規範的。那我若是把代碼寫成下面的這樣呢?
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);
}
}
什麼,您不認爲我直接訪問e.Result,會出現異常嗎?
再來看一下我寫的事件參數類型吧,看看我是如何作的:
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()方法的實現以下(微軟實現的):
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模式中完成事件中的標準處理代碼:
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模式來講:是標準的處理方式:首先要判斷this.Error != null ,爲何這個 不規範 的方式會成爲標準呢?
我要再問一句:爲何不用try.....catch這種更規範的處理方式呢?
顯然,我也演示了:EAP模式在獲取結果時,也能夠支持try.....catch這種方式的。在這裏不用它的理由是由於:
相對於if判斷這類簡單的操做來講,拋異常是個【昂貴】的操做。這種明顯能夠提升性能的作法,難道有錯嗎?
在.net設計規範中,還有Tester-Doer, Try-Parse這二類模式。我想不少人也應該用過的吧,設計它們也是由於性能問題,與EAP的理由是同樣的。
再來總結一下。個人CallCompletedEventArgs類在實現時,有二個關鍵點:
1. 事件類型要從AsyncCompletedEventArgs繼承。
2. 用只讀屬性返回結果,但在訪問前,要調用基類的base.RaiseExceptionIfNecessary();
這些都是EAP模式中,正確的設計方式。什麼是模式?這就是模式。什麼是規範?這就是規範!
咱們不能由於錯誤的設計,或者說,不尊守規範的設計,而形成的缺陷也要怪罪於EAP 。
結束語
異步是個頗有用的技術,無論是對於桌面程序仍是服務程序都是很用意義的。
不過,相對於同步調用來講,異步也是複雜的,但它的各類使用方式也是很精彩的。
異步很精彩,故事沒講完,請繼續關注個人後續博客。
轉自http://www.cnblogs.com/fish-li/archive/2011/11/20/2256385.html