細說ASP.NET的各種異步操作

在上篇博客【C#客戶端的異步操作】,我介紹了一些.net中實現異步操作的方法,在那篇博客中,我是站在整個.net平臺的角度來講述各種異步操作的實現方式,並針對各種異步操作以及不同的編程模型給出了一些參考建議。上篇博客談到的內容可以算是異步操作的基礎,今天我再來談異步,專門來談在ASP.NET平臺下的各種異步操作。在這篇博客中,我主要演示在ASP.NET中如何使用各種異步操作。
在後續博客中,我還會分析ASP.NET的源碼,解釋爲什麼可以這樣做,或者這樣的原因是什麼,以解密內幕的方式向您解釋這些操作的實現原理。

由於本文是【C#客戶端的異步操作】的續集,因此一些關於異步的基礎內容,就不再過多解釋了。如不理解本文的示例代碼,請先看完那篇博文吧。

【C#客戶端的異步操作】的結尾,有一個小節【在Asp.net中使用異步】,我把我上次寫好的示例做了個簡單的介紹,今天我來專門解釋那些示例代碼。不過,在寫博客的過程中,又做了一點補充,所以,請以前下載過示例代碼的朋友,你們需要重新下載那些示例代碼(還是那篇博客中)。
說明:那些代碼都是在示範使用異步的方式調用【用Asp.net寫自己的服務框架】博客中所談到的那個服務框架,且服務方法的代碼爲:


[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());
}

在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 BeginProce***equest(HttpContext context, AsyncCallback cb, object extraData);
//
// 摘要:
// 進程結束時提供異步處理 End 方法。
//
// 參數:
// result:
// 包含有關進程狀態信息的 System.IAsyncResult。
void EndProce***equest(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 Proce***equest(HttpContext context)
{
// 注意:這個方法沒有必要實現。因爲根本就不調用它。
// 但要保留它,因爲這個方法也是接口的一部分。
throw new NotImplementedException();
}

public IAsyncResult BeginProce***equest(HttpContext context, AsyncCallback cb, object extraData)
{
// 說明:
// 參數cb是一個ASP.NET的內部委託,EndProce***equest方法將在那個委託內部被調用。

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 EndProce***equest(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. 在BeginProce***equest()方法,調用要你要調用的異步開始方法,通常會是另一個BeginXxxxx方法。
2. 在EndProce***equest()方法,調用要你要調用的異步結束方法,通常會是另一個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#" style="color:red; font-weight: bold;">Async style="font-size: x-large">="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 DirectProce***equestMoudle : 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章