通過 ASP.NET 異步編程實現可擴展的應用程序

代碼: http://download1.csdn.net/down3/20070521/21225715844.rar
目錄
邊欄




您想了解祕密嗎?諱莫如深,不可言傳的祕密?一旦揭示,將在 ASP.NET 社區引起巨大的反響,並使 Microsoft 的反對者發出“啊哈!”的驚歎,對嗎?

多數使用 ASP.NET 構建的網站沒有良好的可擴展性。它們受到自我強加的“玻璃天花板”的制約,這種束縛限制了它們每秒可處理的請求的數量。這些站點的擴展性一直良好,直到流量提升到這一無形限制時。然後吞吐量開始下降。很快,請求開始失敗,通常返回“服務器不可用”錯誤。

《MSDN®雜誌》曾多次就其根本原因進行過討論。ASP.NET 使用公共語言運行庫 (CLR) 線程池中的線程來處理請求。只要在線程池中存在可用線程,ASP.NET 調度傳入請求就不會有任何麻煩。但是一旦線程池處於飽和狀態(即所有池中的線程忙於處理請求,而沒有可用的線程),則新的請求必須等待線程可用。如果這種僵局變得相當嚴重、隊列到達容量限制,ASP.NET 將束手無策,對於新的請求只能做出“拒絕”響應。

一種解決方法是提高線程池的上限,以創建更多的線程。這是當其客戶報告頻繁遇到“服務器不可用”錯誤時,開發人員經常採取的方法。另一種經常採用的方法是放棄出現問題的硬件,向 Web 場中添加更多的服務器。但是,增加線程數或服務器數並不能從根本上解決這一問題。實際上,它僅僅暫時緩解了存在的設計問題,並非存在於 ASP.NET 中,而是實際站點實現中存在的問題。對於不能擴展的應用程序,實際的問題是線程的缺乏。不能有效使用已存在的線程是問題的所在。

真正可擴展的 ASP.NET 網站充分利用了線程池。這意味着可確保請求處理線程執行代碼,而非等待 I/O 完成。如果由於所有線程都在消耗 CPU 而造成線程池飽和,除了添加服務器,您幾乎無計可施。

然而,多數 Web 應用程序可以與數據庫、Web 服務或其他外部實體進行通話,並通過強制線程池等待完成數據庫查詢、Web 服務調用和其他 I/O 操作來限制可擴展性。針對數據驅動的網頁的查詢可能要花費千分之幾秒來執行代碼,花幾秒鐘等待數據庫查詢返回。當查詢未完成時,分配給請求的線程無法服務於其他的請求。這就是所謂的玻璃屋頂。如果您要構建具有高度可擴展性的網站,這種情況是您必須避免的。請記住:當涉及吞吐量時,除非處理得當,否則 I/O 會成爲大問題。

當然,如果 I/O 沒有破壞線程池,則算不上大問題。ASP.NET 支持三種可作爲防破壞代理的異步編程模型。對於社區而言,這些模型大都未知,部分原因在於缺乏相關文檔。瞭解如何以及何時使用這些模型對於構建先進的網站絕對至關重要。


異步頁面

ASP.NET 支持的這三種異步編程模型中,首要的、通常也是最有用的是異步頁面。在這三種模型中,這是唯一針對 ASP.NET 2.0 的。其他支持的模型都是針對版本 1.0 的。

在此,我不再詳細介紹異步頁面,因爲在 2005 年 10 月期的雜誌中,我曾對此進行過討論。(msdn.microsoft.com/msdnmag/issues/05/10/WickedCode)。結論是:如果您有一些頁面要執行相對較長的 I/O 操作,它們就應成爲異步頁面。如果某頁面查詢數據庫,花了 5 秒鐘返回(因爲它既返回大量數據,又通過大量加載的連接將目標鎖定到遠程數據庫),線程分配給該請求的 5 秒鐘不可用於其他請求。如果每個請求都照此處理,應用程序將會很快陷入停頓。

圖 1 顯示了異步頁面是如何解決這一問題的。當請求到達時,由 ASP.NET 爲其分配一個線程。該請求開始在該線程中進行處理,當選擇數據庫時,請求將啓動異步 ADO.NET 查詢,並將線程返回到線程池中。當查詢完成時,ADO.NET 回調到 ASP.NET,ASP.NET 從線程池中調出另一個線程,並恢復處理請求。

圖 1 有效的異步頁面
圖 1 有效的異步頁面 (單擊該圖像獲得較大視圖)

查詢未完成時,線程池中的任何線程均未使用,以確保所有線程均可用於傳入的請求。異步處理的請求不能被快速執行。但其他請求可更快地執行,因爲它們不必等待線程可用。在進入管道時,請求可引起較少的延遲,整體吞吐量會被提升。

圖 2 顯示了根據 SQL Server™ 數據庫執行數據綁定的異步頁面的代碼隱藏類。Page_Load 方法調用 AddOnPreRenderCompleteAsync 以註冊開始和結束處理程序。在請求生存期的末期,ASP.NET 調用 Begin 方法,該方法將啓動異步 ADO.NET 查詢並即刻返回,於是,分配給該請求的線程將返回線程池。當 ADO.NET 表明查詢已經結束時,ASP.NET 將從線程池中檢索線程(不必和以前使用的相同),並調用 End 方法。End 方法獲得查詢結果,請求的其餘部分在執行 End 方法的線程中正常執行。

圖 2 中未顯示的內容是 ASPX 的 Page 指令中的 Async="true" 屬性。異步頁面應能夠:提示 ASP.NET 在頁面中實現 IHttpAsyncHandler 接口(稍後將詳細介紹)。同樣未在圖 2 中顯示的是數據庫連接字符串,該字符串包含自己的 Async="true" 屬性,這樣,ADO.NET 就知道要執行異步查詢了。

AddOnPreRenderCompleteAsync 是構建異步頁面的一種方法。另一種方法是調用 RegisterAsyncTask。與 AddOnPreRenderCompleteAsync 方法相比,這種方法具有一些優勢,最重要的是它簡化了在一個請求中執行多個異步 I/O 操作的任務。有關於此的詳細信息,請參閱 2005 年 10 月期的“超酷代碼”部分。

Back to top

異步 HTTP 處理程序

ASP.NET 中的第二個異步編程模型是異步 HTTP 處理程序。HTTP 處理程序是一個作爲請求終結點的對象。例如,對 ASPX 文件的請求由針對 ASPX 文件的 HTTP 處理程序處理。同樣,對 ASMX 文件的請求由知道如何處理 ASMX 服務的 HTTP 處理程序處理。事實上,ASP.NET 擁有針對多種文件類型的 HTTP 處理程序。在 web.config 主文件的 <httpHandlers> 部分(在 ASP.NET 1.x中,其位於 machine.config 中),您可以看到這些文件類型和相應的 HTTP 處理程序。

通過編寫自定義 HTTP 處理程序,您可以擴展 ASP.NET 以支持其他文件類型。但是,更有趣的一點是,您可以在 ASHX 文件中部署 HTTP 處理程序,並將它們用作 HTTP 請求的目標。這是構建動態生成圖像或從數據庫中檢索圖像的 Web 端點的正確方法。您只需將 <img> 標記(或 Image 控件)包含在頁面中,並將其指向創建或獲取圖像的 ASHX。將目標鎖定到帶有請求的 ASHX 文件比將目標鎖定到 ASPX 文件更有效,因爲 ASHX 文件在處理時開銷更少。

根據定義,HTTP 處理程序可實現 IHttpHandler 接口。實現該接口的處理程序不能同步進行處理。圖 3 中的 ASHX 文件包含一個這類 HTTP 處理程序。在運行時,TerraServiceImageGrabber 在 Microsoft® TerraServer Web 服務之外進行多次調用以將城市和州轉換爲經度和緯度,檢索衛星圖像(如同一塊塊“瓷磚”),然後將圖像拼接在一起形成指定位置的複合圖像。

結果如圖 4 所示。所顯示的頁面包含 Image 控件,其 ImageUrl 屬性將目標鎖定在圖 3 中所顯示的 ASHX 文件。當用戶選擇了城市和州並單擊按鈕後,HTTP 處理程序則將輸入轉換爲衛星圖像。

結果令人印象深刻。但這有一個問題。TerraServiceImageGrabber 是如何避免編寫 HTTP 處理程序的完美示例。想一想。TerraServiceImageGrabber 需要幾秒鐘(至少)完成其所有 Web 服務調用並處理結果。大部分時間僅僅花費在等待 Web 服務調用完成上。對於 ASHX 文件的重複請求會轉瞬間耗盡 ASP.NET 線程池,阻止應用程序中其他頁面的使用(或者至少使它們排隊等待線程可用)。您不能用這種方法構建可擴展的應用程序,除非您擴展了硬件。但是,當使用正確編寫的軟件通過一臺服務器就能處理負載時,爲什麼還要將成千上萬的資金耗費在 Web 場上呢?

圖 4 有效的 TerraServiceImageGrabber
圖 4 有效的 TerraServiceImageGrabber (單擊該圖像獲得較大視圖)

HTTP 處理程序不必是同步的。通過實現 IHttpAsyncHandler 接口,該接口本身可從 IHttpHandler 派生出來,HTTP 處理程序可以是異步的。如果正確使用,異步處理程序可更有效地利用 ASP.NET 線程。這可採用與異步頁面相同的方式來完成。事實上,異步頁面可利用在 ASP.NET 中將異步頁面日期提前的異步處理程序支持。

圖 5 包含圖 3 所示的處理程序的異步版本。Async-TerraServiceImageGrabber 稍微有點複雜,但具有更高的可擴展性。

當 ASP.NET 調用處理程序的 BeginProcessRequest 方法時,開始異步處理。通過 TerraService 代理的 BeginConvertPlaceToLonLatPt 方法,BeginProcessRequest 可對 TerraService 進行異步調用。然後,分配給該請求的線程返回線程池中。異步調用完成時,另一個線程被從線程池中調出以執行 ConvertPlaceToLonLatCompleted 方法。該線程會檢索上次調用的結果,進行自己的異步調用,然後返回線程池。這種模式不斷重複直至所有異步調用完成,此時,調用處理程序的 EndProcessRequest 方法,產生的位圖被返回給請求者。

要阻止 EndProcessRequest 直至最後的 Web 服務調用完成,AsyncTerraServiceImageGrabber 返回來自 BeginProcessRequest 的 IAsyncResult 的自我實現。如果它要返回由 BeginConvertPlaceToLonLatPt 返回的 IAsyncResult,則在第一個 Web 服務調用完成時,需調用 EndProcessRequest(並終止請求)。

實現 IAsyncResult 和 TerraServiceAsyncResult 的類具有可隨時調用以完成請求的公共 CompleteCall 方法。通常,只有在最後的 Web 服務調用完成後,AsyncTerraServiceImageGrabber 才調用 CompleteCall。不過,如果在 BeginProcessRequest 和 EndProcessRequest 之間執行的某一方法拋出異常,處理程序將異常緩存在私有字段 (_ex) 中,調用 CompleteCall 以終止請求,然後從 EndProcessReques 中重新拋出異常。否則,異常將丟失,請求將無法完成。

由於 AsyncTerraServiceImageGrabber 使用 ASP.NET 線程的時間只是處理請求所需的總時間的一小部分,因此,AsyncTerraServiceImageGrabber 比其同步版的同類方法具有更高的可擴展性。大部分時間裏,它只是等待異步 Web 服務調用完成。

理論上,AsyncTerraServiceImageGrabber 還勝過 TerraServiceImageGrabber,因爲它不是順序地重複調用 TerraService's GetTile 方法,而是並行調用。不過,實際上,每次只有兩個針對給定 IP 地址的出站調用可以被掛起,除非您提高了運行庫的默認 maxconnection 設置:

<system.net>
  <connectionManagement>
    <add address="*" maxconnection="20" />
  </connectionManagement>
</system.net>

 

其他配置設置也可影響併發。有關詳細信息,請參考知識庫文章“從 ASP.NET 應用程序進行 Web 服務請求時出現的爭用、性能不佳和死鎖等問題”(support.microsoft.com/kb/821268)。

即使每次只執行一個調用,但 AsyncTerraServiceImageGrabber 並不比 TerraServiceImageGrabber 差。它的設計非常出色,因爲它儘可能有效地使用了 ASP.NET 線程。

Back to top

異步 HTTP 模塊

您在 ASP.NET 中可能利用的第三個異步編程模型是異步 HTTP 模塊。HTTP 模塊是位於 ASP.NET 管道中的對象,在管道中,它可以查看甚至修改傳入請求和傳出響應。ASP.NET 中的許多主要服務都是以 HTTP 模塊的形式實現的,包括身份驗證、授權和輸出緩存。通過編寫自定義 HTTP 模塊並將它們插入管道,您可以擴展 ASP.NET。當您這樣做的時候,一定要認真考慮這些 HTTP 模塊是否應當是異步的。

圖 6 包括稱爲 RequestLogModule 的簡單、同步 HTTP 模塊的源代碼,它在名爲 RequestLog.txt 的文本文件中記錄了傳入請求。在站點的 App_Data 目錄下創建該文件,這樣用戶就無法瀏覽它。(要注意 ASP.NET 作爲安全主體的運行(例如,ASPNET 或網絡服務)必須寫入對 App_Data 的使用權限。)該模塊實現 IHttpModule 接口,這是 HTTP 模塊的唯一要求。加載該模塊時,其 Init 方法會爲 HttpApplication.PreRequestHandlerExecute 事件註冊一個處理程序,該程序從每個請求的管道中被觸發。事件處理程序打開 RequestLog.txt(或在該文件不存在的情況下創建一個),然後將一行包含關於當前請求的有針對性的信息寫入其中,包括請求到達的時間和日期、請求者的用戶名(如果請求是要進行身份驗證的,或者如果身份驗證關閉,則要包含請求者的 IP 地址),以及請求的 URL。該模塊在 web.config 的 <httpModules> 部分進行註冊,以便在每次應用程序啓動時,提示 ASP.NET 加載該文件。

RequestLogModule 存在兩方面的問題。首先,每次請求時均要執行 I/O 文件。其次,它使用請求處理線程來執行 I/O,否則,線程可能被用於爲其他傳入請求服務。由於簡單,該模塊會導致吞吐量損失。通過批處理 I/O 文件操作,您可能會緩解延遲,更好的方法是使模塊異步(或者最好批處理 I/O 文件並使模塊異步)。

圖 7 顯示了異步版本的 RequestLogModule。調用 AsyncRequestLogModule 後,它將執行完全相同的工作,並將分配給請求的線程返回線程池,然後寫入文件。當寫入完成時,從線程池中調出新的線程,用於完成請求。

如何使 AsyncRequestLogModule 異步?其 Init 方法調用 HttpApplication.AddOnPreRequestHandlerExecuteAsync 以便爲 PreRequestHandlerExecute 事件註冊 Begin 和 End 方法。HttpApplication 類包含針對其他 per-request 事件的其他 AddOn 方法。例如,HTTP 模塊可以調用 AddOnBeginRequestAsync 以便爲 BeginRequest 事件註冊異步處理程序。AsyncRequestLogModule 的 BeginPreRequestHandlerExecute 方法使用 Framework 的 FileStream.BeginWrite 方法來開始異步寫入。BeginPreRequestHandlerExecute 返回時,線程返回線程池。

AsyncRequestLogModule 包含一些值得特別一提的線程同步邏輯。運行在多個線程中的多個請求可能要同時寫入日誌文件。爲了確保併發寫入不會相互覆蓋,AsyncRequestLogModule 在由所有模塊實例共享的私有字段中保存了下一個寫入在文件中的位置 (_position)。每次調用 BeginWrite 之前,模塊從字段中讀取該位置並更新字段以指向要寫入該文件的內容的第一個字節。讀取並更新 _position 的邏輯包含在 lock 語句中,這樣每次就有不止一個線程可執行它。這防止了在一個線程有機會更新位置之前,另一個線程讀取該位置。

現在,談談不足之處。對於未使用線程池中另一個線程的 BeginWrite,FileStream 構建函數的 isAsync 參數必須設置爲“true”,正如我在示例中所做的那樣。不過,使用 FileStream.BeginWrite 啓動異步寫入的一個已知結果是無法保證寫入實際上是異步的,即使您已經請求異步操作。如果確信同步 I/O 更快,Windows® 保留同步執行異步 I/O 文件的權利。有關詳細信息,請參閱 support.microsoft.com/kb/156932 上的知識庫文章。好的方面是如 Windows 同步寫入請求日誌,理論上講,寫入可以更快執行,這樣它們對主機應用程序可擴展性造成的影響最小。

Back to top

總結

異步編程是儘可能高效地使用 ASP.NET 線程池來構建擴展性更強的應用程序的一種很好的方法。以往,我很少看到 ASP.NET 開發人員使用異步編程模型,部分原因在於他們並不知道存在這些模型。不要讓稀疏文檔成爲您的“攔路虎”,從現在起就開始異步思考,今後您將會構建出更好的應用程序。

請注意,本文提供了 C# 和 Visual Basic® 版本的可下載示例代碼。我常常收到要求提供 Visual Basic 版示例的電子郵件。這一次,您不必再問了,我已經提供了該版本的示例!

 
發佈了22 篇原創文章 · 獲贊 3 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章