對於搞asp.net的程序員,都知道所有的服務請求最終都會有一個IhttpHandler來處理,就像我們最常用的aspx頁面。相對於IHttpHandler,asp.net還提供了一個異步的相同版本的處理程序接口,它就是IHttpAsyncHandler,同樣asp.net也可以讓我們的aspx頁面實現IHttpAsyncHandler,而不僅僅是IHttpHandler。
為什么要異步頁面
我們都知道asp.net維護一個處理頁面請求的線程池,每一個新的請求,asp.net就會從其中取出一個空閑的線程來實例化頁面,運行處理代碼然后呈現HTML,然后返回線程池,等待下一次被激活。但是如果請求到來的過於頻繁,比我們線程處理頁面返回時間還短,那么這個請求就會被放到一個隊列里,如果隊列滿了,就會產生一個503的服務不可用來拒絕其它的請求。
可以想象,如果我們的頁面在等待一個慢的服務器在處理大量的數據、讀取遠程文件或一個WEB服務返回數據,這時我們頁面沒有代碼要執行,但是這個線程會被掛起,這就會嚴重的消耗可用線程,影響網站並發。
而通過異步頁面, 我們可以把這些耗時的處理遷移到其它線程池,而這些異步工作完成時,asp.net會接到通知,再次從線程池激活一個可用線程,處理余下的工作,最終呈現HTML。
創建異步頁面
創建異步頁面,遠比我們想象簡單的多,我們首先要在Page指令加一個Async的特性,並把它設為true.
<%@ Page Async="true" AsyncTimeout="60" ...
還有一個timeout的特性用來指定異步的超時時間,單位是s,默認是45s。
接下來,我們需要調用AddOnPreRenderCompleteAsync注冊異步處理:
protected void Page_Load(object sender, EventArgs e) { AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginHandler), new EndEventHandler(EndHandler)); }
AddOnPreRenderCompleteAsync還提供另一個重載的版本
public void AddOnPreRenderCompleteAsync( BeginEventHandler beginHandler, EndEventHandler endHandler, Object state )
首先,開始啟動異步任務的委托和處理異步結束時的回調是不可少的,另一個參數,讓我們可以傳遞一些狀態的信息給異步開始的方法。
異步頁面的執行
在我們展示BeginHandler、EndHandler之前,讓我們通過下面轉載自MSDN的一張圖,看一下異步處理是如何工作的:
我們可以看出我們注冊的BeginHandler在prePrend之后才開始執行,這時線程已經回到線程池,代碼的處理交到了BeginHandler,我們必須在這里開始一個異步的處理,處理完后返回IAsyncResult的結果,隨后EndHandler被調用,之后,線程池的另一個線程被激活,接着處理頁面流程。
有效的異步處理
到了這里,你可能感覺到異步頁面分明就是一個坑啊,到了最后還是要我們自己去實現異步處理一個耗時的操作。
但是這可能對於我們來說算不上什么啊,我們有很多種方法開始異步的處理啊,ThreadPool.QueueUserWorkItem,Thread類創建一個專用線程、委托的BeginInvoke和類庫中內建的異步支持,如Command的BeginExecuteReader,但是我們能選擇的卻不是那么多。
第一類,委托的BeginInvoke和ThreadPool.QueueUserWorkItem,這兩個會從asp.net請求線程池中激活線程來處理,這就是相同於釋放一個線程的同時又從線程池拿一個線程出來,這不是脫褲子放屁嗎?一點也起不到增強網站並發處理的能力,還無謂的增加了線程調度的浪費。
第二類,Thread類創建專用線程,這樣做可以達到目的,並且可以做到服務器不能處理的工作,但這是非常危險的。如果這樣的請求過於和頻繁,創建出過多的這樣的線程,這對服務器是一種壓力,很可能導致服務器再也不能處理其它的請求了。當然,你可以實現一個自定義的線程池來管理這些線程,讓他保持在一個合適的范圍,並且總是有可用的線程可用,但是這個開發代價就太大了。
接下來就只有.net內置如數據Command的BeginExecuteReader、IO的BeginRead和BeginWriter等處理異步的支持了,其實這也是我們最應該也最值得用的異步方式。讓.net去管理線程的問題,又不會從當前請求線程池中拿線程,使用起來也簡單強大。
異步的實現
接着上面的代碼,我們貼出BeginHandler、EndHandler代碼,只是提供一個例子:
private SqlConnection con; private SqlCommand cmd; private IAsyncResult BeginHandler(Object obj, EventArgs args, AsyncCallback cb, Object state) { string conStr = ""; con = new SqlConnection(conStr); cmd = new SqlCommand("select * from ...", con); con.Open(); return cmd.BeginExecuteReader(cb, state); } private void EndHandler(IAsyncResult ar) { try { SqlDataReader reader = cmd.EndExecuteReader(ar); …………… } catch (Exception ex) { // 錯誤處理 } }
這樣就實現了一個簡單的異步頁面的模型,對於這些耗時的操作,我們可能會使用到緩存,這樣我們自定義一個實現了IAsyncResult的類,包含我們要使用的數據,在BeginHandler里判斷緩存是否存在,如果存在返回自定義實例,並用緩存填充這個實例,不存在就執行異步操作;而在EndHandler里區分出返回實例,使用數據再更新緩存。
多異步任務
如果要處理多個Web服務或者同時去等待web服務,還有數據庫操作等等,這時,我們怎么做?
1,我們可以調用多次AddOnPreRenderCompleteAsync,每次傳入對應的begin和end的委托,但是注冊的多個任務是順序執行的,也就是只有處理完第一個任務end執行過后,才會開始執行第二個任務。
2,我們只調用一次AddOnPreRenderCompleteAsync,在begin里啟動多少異步操作,但是這個操作會有太多的局限性,並且會更復雜。
3,不出大家意外,asp.net提供了處理這樣的方法。就是如下的方式:
PageAsyncTask taska = new PageAsyncTask( new BeginEventHandler(BeginHandler1), new EndEventHandler(EndHandler1), null, null ); Page.RegisterAsyncTask(taska); PageAsyncTask taskb = new PageAsyncTask( new BeginEventHandler(BeginHandler2), new EndEventHandler(EndHandler2), null, null ); Page.RegisterAsyncTask(taskb);
這樣的注冊異步任務會同時執行,當所有的異步都執行完畢,才會開始余下的頁面的流程。
最后我們看一下PageAsyncTask的重載版本:
public PageAsyncTask ( BeginEventHandler beginHandler, EndEventHandler endHandler, EndEventHandler timeoutHandler, Object state, bool executeInParallel )
除了前兩個任務開始和結束調用操作參數之外,還提供了一個超時時的處理程序、一個球表示任務狀態的對象和一個是否要和其它任務同時執行的布爾值。
最后
沒有最好,只有最適合,任何一種的處理方式都不會是完美的,就像Jeffrey大師在異步操作中所說的:應盡可能限制線程的使用。異步頁面雖然可以讓我們網站可以處理更多的請求,但是它並不會讓你的用戶感覺到頁面的呈現變快,甚至會慢些,因為創建線程本身就會產生一定的消耗,並且線程之間的切換開銷也相當大。切記。