對於搞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大師在非同步作業中所說的:應儘可能限制線程的使用。非同步頁面雖然可以讓我們網站可以處理更多的請求,但是它並不會讓你的使用者感覺到頁面的呈現變快,甚至會慢些,因為建立線程本身就會產生一定的消耗,並且線程之間的切換開銷也相當大。切記。