ASP.NET 2.0 中的非同步頁面

來源:互聯網
上載者:User
 

ASP.NET 2.0 中的非同步頁面 (2007-10-14 09:33:30)  
分類: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 極大地簡化了非同步頁面的構建過程。要開始構建非同步頁面,首先要在頁面的 @ 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 響應。


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

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


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

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

非同步資料繫結

對 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 來呈現資料庫查詢結果。但是從內部看,該頁面更加具有延展性,因為它並未停留在一個等待查詢返回的線程池線程上。

非同步呼叫 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 引擎會在最後一次調用返回後才呈現頁面。

非同步任務

要在一個非同步頁面中進行多次 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 中,實現非同步頁面將變得更加便捷,其架構允許您在一個請求中批量執行多次非同步 I/O 操作,並且您可以在所有操作完成後再呈現頁面。非同步 ASP.NET 頁面結合非同步 ADO.NET 和 .NET Framework 中的其他非同步功能,提供了一種強大而便捷的方案來解決 I/O 密集型請求由於線程池擁擠而導致延展性受限的問題。

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.