天天看點

ASP.NET 2.0 中的異步頁面

ASP.NET 2.0 中的異步頁面

(2007-10-14 09:33:30)  

<script> var $tag='IT/科技'; var $tag_code='da882273f48395ff57796f357900391a'; </script> 分類:asp.net

ASP.NET 2.0 提供了多種新的功能,從聲明性資料綁定和母版頁到成員資格和角色管理服務,一應俱全。但是我認為最酷的新功能則是異步頁面,下面就讓我來告訴您原因。

當接收到一個頁面請求時,ASP.NET 會從一個線程池中擷取一個線程,并将頁面請求配置設定給該線程。一個普通的,或者說是同步的頁面在請求期間會占用線程,以防止線程被用于處理其他請求。如果同步請求變為 I/O 密集狀态,例如,當該請求調用一個遠端 Web 服務或查詢遠端資料庫并等待調用傳回時,則配置設定給它的線程在調用傳回前會始終處于閑置狀态。這種情況會限制可伸縮性,因為線程池中的可用線程是有限的。如果處理請求的所有線程都因等待 I/O 操作的完成而阻塞,則會有多餘的請求排隊等待這些線程的釋放。最好的情況是出現吞吐量降低,因為需要等待更長的等待才能處理請求。最糟糕的情況是隊列被填滿而 ASP.NET 無法處理後續請求,并提示 503“伺服器不可用”錯誤。

異步頁面的出現為解決 I/O 密集型的請求所導緻的此類問題提供了簡潔的方案。頁面處理要線上程池中的一個線程上進行,但是當一個異步 I/O 操作響應來自 ASP.NET 的信号并開始進行時,該線程會傳回原先的線程池。操作完成後,ASP.NET 會從線程池中擷取另一個線程來完成處理請求。這樣,線程池的線程使用率得到提高,可伸縮性也是以得以增強。那些本來要等待 I/O 操作完成而阻塞的線程此時可以用于處理其他請求。這樣做的直接好處就是避免請求執行冗長的 I/O 操作,是以可以快速進出管道。等待進入管道的時間過長會對此類請求的執行造成的很大的負面影響。

目前有關 ASP.NET 2.0 Beta 2 異步頁面基礎架構的文章相對較少。為了解決這一問題,讓我們來了解一下異步頁面的知識。請注意,本專欄内容是基于 ASP.NET 2.0 和 .NET Framework 2.0 的測試版的。

ASP.NET 1.x 中的異步頁面

ASP.NET 1.x 本身并不支援異步頁面,但是隻要一點耐心和想象力就可以建構它們。要深入了解有關内容,請參閱 Fritz Onion 發表在 2003 年 6 月份的《MSDN雜志》上的文章“在您的伺服器端 Web 代碼中使用線程并建構異步處理程式”。

技巧就在于在頁面的代碼隐藏類中實作 IHttpAsyncHandler,使 ASP.NET 不再調用頁面的 IHttpHandler.ProcessRequest 方法,而是通過調用 IHttpAsyncHandler.BeginProcessRequest 來處理各種請求。這樣在您的 BeginProcessRequest 實作部分就可以啟動另一個線程。該線程調用 base.ProcessRequest,使得頁面在一個非線程池線程上對請求進行正常處理(諸如 Load 事件和 Render 事件等全部包括)。同時,BeginProcessRequest 在啟動了新線程後立即傳回,使得執行 BeginProcessRequest 的線程能夠傳回線程池。

以上隻是基本原理,但是具體的細節卻遠不止這些。除此之外,您還需要執行 IAsyncResult 并在 BeginProcessRequest 将其傳回。這顯然意味着要建立一個 ManualResetEvent 對象,并當背景線程中傳回 ProcessRequest 時向該對象發送信号。此外,您需要一個線程來調用 base.ProcessRequest。不幸的是,大多數能夠将工作轉移至背景線程的傳統技術,包括 Thread.Start、ThreadPool.QueueUserWorkItem 和異步委托,都無法在 ASP.NET 應用程式中達到預期效果,因為它們要麼會從線程池中竊取線程,要麼有可能造成線程無限制地增長。正确實作異步頁面需要使用自定義的線程池,而編寫自定義線程池也不是件容易的事。(有關詳細資訊,請參閱 2005 年 2 月份的《MSDN 雜志》中的“.NET 相關問題”專欄)。

坦白講,在 ASP.NET 1.x 中建構異步頁面并非天方夜譚,但是要做到這一點非常麻煩。而且在嘗試過這種滋味後,您會情不自禁地渴望一種更好的解決辦法。現在我們有了解決方法,那就是 ASP.NET 2.0。

ASP.NET 2.0 中的異步頁面

ASP.NET 2.0 中的異步頁面

ASP.NET 2.0 極大地簡化了異步頁面的建構過程。要開始建構異步頁面,首先要在頁面的 @ Page 指令中添加如下的 Async="true" 的屬性:

<%@ Page Async="true" ... %>      

究其本質,這段代碼的作用是告訴 ASP.NET 在頁面中執行 IHttpAsyncHandler。接下來,您需要在頁面生存期的早期(例如,在 Page_Load 期間)調用新的 Page.AddOnPreRenderCompleteAsync 方法,以注冊一個 Begin 方法和一個 End 方法,如以下代碼所示:

AddOnPreRenderCompleteAsync ( new BeginEventHandler(MyBeginMethod), new EndEventHandler (MyEndMethod) );      

接下來是精彩的部分。頁面繼續進行正常的處理過程,直至稍後觸發 PreRender 事件。ASP.NET 會調用先前使用 AddOnPreRenderCompleteAsync 注冊的 Begin 方法。Begin 方法的作用是啟動一項諸如資料庫查詢或 Web 服務調用的異步操作并立即傳回。此時,配置設定給請求的線程也會傳回到線程池中。此外,Begin 方法還會傳回一個 IAsyncResult,它能夠讓 ASP.NET 确定何時完成異步操作,以便 ASP.NET 能夠在這一時刻從線程池提取線程并調用 End 方法。當 End 傳回後,ASP.NET 執行包括呈現階段在内的頁面生存期的剩餘部分。在 Begin 傳回後與 End 被調用前的這段時間内,處理請求的線程處于空閑狀态,可以為其他請求提供服務,直到 End 被調用,呈現被顯示。并且由于 .NET Framework 2.0 版提供多種執行異步操作的途徑,您甚至在多數情況下無需執行 IAsyncResult。Framework 會為您執行它。

圖 1 中的代碼隐藏類為我們提供了一個示例。相應的頁面包含一個 Label 控件,其 ID 為“Output”。該頁面使用 System.Net.HttpWebRequest 類來擷取 http://msdn.microsoft.com 上的内容。随後它對傳回的 HTML 進行分析并向 Label 控件中寫入一個清單,其中列出了它所找到的所有 HREF 目标。

由于 HTTP 請求需要很長時間才能傳回,AsyncPage.aspx.cs 會異步執行處理。它會在 Page_Load 中注冊 Begin 方法和 End 方法,并在 Begin 方法中調用 HttpWebRequest.BeginGetResponse 來啟動一個異步 HTTP 請求。BeginAsyncOperation 将 BeginGetResponse 傳回的 IAsyncResult 傳回至 Asp.NET,進而 ASP.NET 能夠在 HTTP 請求完成時調用 EndAsyncOperation。接着,EndAsyncOperation 将對内容進行分析,并将結果寫至 Label 控件,随後進行呈現,并向浏覽器傳回一個 HTTP 響應。

ASP.NET 2.0 中的異步頁面

圖 2  同步頁面處理與異步頁面處理

圖 2 說明了 ASP.NET 2.0 中同步頁面與異步頁面的不同之處。當同步頁面被請求時,ASP.NET 會為該請求配置設定一個來自線程池的線程,并在此線程上執行該頁面。如果該請求暫停并轉而執行一項 I/O 操作,則此線程會被占用直至 I/O 操作完成。這樣頁面的整個生命周期才算完成。比較而言,異步頁面是通過 PreRender 事件正常執行的。随後,使用 AddOnPreRenderCompleteAsync 注冊的 Begin 方法将被調用,之後用于處理請求的線程會傳回線程池。Begin 會啟動一個異步 I/O 操作。操作完成後,ASP.NET 從線程池擷取另一線程并調用 End 方法,在此線程上執行頁面生命周期的剩餘部分。

ASP.NET 2.0 中的異步頁面

圖 3  跟蹤輸出功能顯示了異步頁面的異步點

對 Begin 的調用就是頁面的“異步點”。圖 3 中的跟蹤顯示了異步點出現的準确位置。如果要調用 AddOnPreRenderCompleteAsync,則必須在異步點前調用,也就是說,其調用不得晚于頁面的 PreRender 事件。

ASP.NET 2.0 中的異步頁面

異步資料綁定

對 ASP.NET 頁面而言,直接使用 HttpWebRequest 來請求其他頁面的現象并不常見,但對資料庫的查詢卻是屢見不鮮,而且資料通常會與結果綁定。那麼如何使用異步頁面進行異步資料綁定呢?圖 4 中的代碼隐藏類為我們提供了一種實作綁定的方法。

AsyncDataBind.aspx.cs 使用的是 AsyncPage.aspx.cs 所使用的 AddOnPreRenderCompleteAsync 模式。但是,AsyncDataBind.aspx.cs 的 BeginAsyncOperation 方法并不調用 HttpWebRequest.BeginGetResponse,而是調用 ADO.NET 2.0 中新增的 SqlCommand.BeginExecuteReader 來執行異步資料庫查詢。調用完成後,EndAsyncOperation 會調用 SqlCommand.EndExecuteReader 來擷取一個 SqlDataReader,後者随後被儲存在一個私有字段中。PreRenderComplete 事件(在異步操作完成後與頁面呈現之前的這段時間裡觸發)的事件處理程式中,SqlDataReader 被綁定至 Output GridView 控件。表面上看,這時的頁面貌似一個很正常的同步頁面,它使用 GridView 來呈現資料庫查詢結果。但是從内部看,該頁面更加具有可伸縮性,因為它并未停留在一個等待查詢傳回的線程池線程上。

ASP.NET 2.0 中的異步頁面

異步調用 Web 服務

ASP.NET 網頁經常執行的另一項與 I/O 有關的任務是調用 Web 服務。由于 Web 服務調用需要很長時間才能傳回,執行這些調用的頁面也就成為異步處理的理想之選。

圖 5 顯示了一種對調用 Web 服務的異步頁面進行綁定的方法。該方法采用與圖 1 和圖 4 中相同的 AddOnPreRenderCompleteAsync 機制。頁面的 Begin 方法通過調用 Web 服務代理的異步 Begin 方法,啟動一個異步 Web 服務調用。頁面的 End 方法将 Web 方法傳回的一個 DataSet 引用緩存在一個私有字段中,PreRenderComplete 處理程式将 DataSet 綁定至一個 GridView。下列代碼顯示了該調用所針對的 Web 方法,供您參考:

[WebMethod] public DataSet GetTitles () { string connect = WebConfigurationManager.ConnectionStrings ["PubsConnectionString"].ConnectionString; SqlDataAdapter adapter = new SqlDataAdapter ("SELECT title_id, title, price FROM titles", connect); DataSet ds = new DataSet(); adapter.Fill(ds); return ds; }      

這隻是方法之一,但并非唯一。.NET Framework 2.0 Web 服務代理支援兩種異步調用 Web 服務的機制。一種機制是 .NET Framework 1.x 和 2.0 Web 服務代理中特有的在每個方法中使用 Begin 方法和 End 方法。另一種機制是 .NET Framework 2.0 的 Web 服務代理中獨有的新 MethodAsync 方法和 MethodCompleted 事件。

如果一個 Web 服務中包含一個名為 Foo 的方法,則一個 .NET Framework 2.0 版的 Web 服務代理除了具有名為 Foo、BeginFoo 和 EndFoo 的方法外,還包含一個名為 FooAsync 的方法和一個名為 FooCompleted 的事件。您可以通過為 FooCompleted 事件注冊一個處理程式并調用 FooAsync 來對 Foo 進行異步調用,如下所示:

proxy.FooCompleted += new FooCompletedEventHandler (OnFooCompleted); proxy.FooAsync (...); ... void OnFooCompleted (Object source, FooCompletedEventArgs e) { // Called when Foo completes }      

當 FooAsync 啟動的異步調用完成後,會觸發 FooCompleted 事件來調用 FooCompleted 事件處理程式。包裝此事件處理程式 (FooCompletedEventHandler) 的委托和傳遞給該處理程式的第二個參數 (FooCompletedEventArgs) 都是與 Web 服務代理一同生成的。您可以通過 FooCompletedEventArgs.Result 通路 Foo 的傳回值。

圖 6 所示的代碼隐藏類使用 MethodAsync 模式對 Web 服務的 GetTitles 方法進行異步調用。從功能上講,此頁面與圖 5 中的頁面完全相同。但二者的内部構造卻截然不同。AsyncWSInvoke2.aspx 包含一個類似于 AsyncWSInvoke1.aspx 的 @ Page Async="true" 指令。但 AsyncWSInvoke2.aspx.cs 并不調用 AddOnPreRenderCompleteAsync,而是為 GetTitlesCompleted 事件注冊一個處理程式,并在 Web 服務代理上調用 GetTitlesAsync。ASP.NET 仍然會推遲呈現頁面,直至 GetTitlesAsync 完成。究其本質,這裡用到了 2.0 版本中另一個新增的類(即 System.Threading.SynchronizationContext)的一個執行個體,以接收異步調用開始和完成時的通知。

使用 MethodAsync 而不是 AddOnPreRenderCompleteAsync 來實作異步頁面,有兩個優點。其一,MethodAsync 能夠向 MethodCompleted 事件處理程式傳遞模拟、區域性和 HttpContext.Current。而 AddOnPreRenderCompleteAsync 無法做到這一點。其二,如果頁面進行多次異步調用并且必須在全部調用完成後才能得以呈現,那麼要使用 AddOnPreRenderCompleteAsync 就必須編寫一個 IAsyncResult,并且在全部調用完成前該 IAsyncResult 無法獲得信号。而使用 MethodAsync 則無需如此大費周章。您隻管進行調用,想調用多少就調用多少,ASP.NET 引擎會在最後一次調用傳回後才呈現頁面。

ASP.NET 2.0 中的異步頁面

異步任務

要在一個異步頁面中進行多次 Web 服務異步調用,并要在全部調用完成後才呈現該頁面,使用 MethodAsync 顯得非常便捷。但如果您想在一個異步頁面中執行多個異步 I/O 操作,并且不希望這些操作不涉及 Web 服務,該如何操作呢?這是否意味着需要重新編寫一個 IAsyncResult 來傳回到 ASP.NET,以便告訴 ASP.NET 最後一次調用于何時完成?幸運的是,不需要這麼做。

在 ASP.NET 2.0 中,System.Web.UI.Page 類引入了另外一種能夠便于異步操作的方法:這就是 RegisterAsyncTask。與 AddOnPreRenderCompleteAsync 相比,RegisterAsyncTask 具有四點優勢。首先,除了 Begin 方法和 End 方法,RegisterAsyncTask 允許您注冊一個逾時方法。如果完成一個異步操作的時間過長,可以調用該逾時方法。您可以在頁面的 @ Page 指令中添加一個 AsyncTimeout 屬性,以聲明的方式設定逾時。AsyncTimeout="5" 将逾時設定為 5 秒。第二點優勢,您可以在一個請求中多次調用 RegisterAsyncTask 來注冊多個異步操作。當使用 MethodAsync 時,ASP.NET 會在全部操作完成後才呈現頁面。第三,您可以使用 RegisterAsyncTask 的第四個參數将狀态傳遞給 Begin 方法。最後一點優勢,RegisterAsyncTask 能夠将模拟、區域性和 HttpContext.Current 傳遞給 End 方法和 Timeout 方法。而如上文所述,使用 AddOnPreRenderCompleteAsync 注冊的 End 方法則無法達到相同的效果。

就其他方面而言,依靠 RegisterAsyncTask 的異步頁面與依靠 AddOnPreRenderCompleteAsync 的異步頁面是相似的。它仍然需要在 @ Page 指令中插入一個 Async="true" 的屬性(或者用程式設計方法将頁面的 AsyncMode 屬性設定為“true”,以達到同樣的目的),仍然通過 PreRender 事件正常執行,此時會對使用 RegisterAsyncTask 注冊的 Begin 方法進行調用,并且會在最後一次操作完成前一直對請求做進一步處理。可以看出,圖 7 中與圖 1 中的代碼隐藏類在功能上是等同的,但是圖 7 中的代碼隐藏類使用的是 RegisterTaskAsync,而并非 AddOnPreRenderCompleteAsync。請注意,當完成 HttpWebRequest.BeginGetRequest 所用時間過長時,會調用名為 TimeOutAsyncOperation 的逾時處理程式。相應的 .aspx 檔案包含一個 AsyncTimeout 屬性,該屬性将逾時間隔設定為 5 秒。還要注意的是,空值被傳遞給 RegisterAsyncTask 的第四個參數,該參數原本要用于向 Begin 方法傳遞資料。

RegisterAsyncTask 的主要優點是允許異步頁面觸發多次異步調用,并在所有調用完成後才呈現頁面。它對于單個異步調用也表現極佳,并且它提供了一個 AddOnPreRenderCompleteAsync 所不具備的逾時選項。如果您所建立的異步頁面僅進行一個異步調用,可以使用 AddOnPreRenderCompleteAsync 或 RegisterAsyncTask。但是對于執行兩次或者多次異步調用的異步頁面,RegisterAsyncTask 能夠極大地簡化您的工作。

由于逾時值是一種基于頁面的設定,并非基于調用,您也許在想是否有可能改變單個調用的逾時值。一句話,這是不可能的。您可以通過程式設計修改頁面的 AsyncTimeout 屬性,進而依次改變各項請求的逾時值,但卻無法為同一請求的不同調用設定不同的逾時值。

ASP.NET 2.0 中的異步頁面

總結

現在您應該對 ASP.NET 2.0 中提供的異步頁面有一個深入的了解。在即将推出的新版 ASP.NET 中,實作異步頁面将變得更加便捷,其架構允許您在一個請求中批量執行多次異步 I/O 操作,并且您可以在所有操作完成後再呈現頁面。異步 ASP.NET 頁面結合異步 ADO.NET 和 .NET Framework 中的其他異步功能,提供了一種強大而便捷的方案來解決 I/O 密集型請求由于線程池擁擠而導緻可伸縮性受限的問題。

在建構異步頁面時您需要記住最後一點,即不要從 ASP.NET 使用的線程池中借用線程來啟動異步操作。例如,在頁面的異步點調用 ThreadPool.QueueUserWorkItem 會适得其反,因為此方法會借用線程池中的線程,導緻沒有線程可用于處理請求。比較而言,調用 Framework 中内建的方法(如 HttpWebRequest.BeginGetResponse 方法和 SqlCommand.BeginExecuteReader 方法)通常是安全的,因為這些方法要使用完成端口來執行異步操作。