最近幾年,非同步編程受到極大關注,主要是出於兩個關鍵原因:首先,它有助於提供更好的使用者體驗,因為不會阻塞 UI 線程,避免了處理結束前出現 UI 介面掛起。其次,它有助於大幅擴充系統,而且無需添加額外硬體。
但是,編寫合適的非同步代碼來管理線程本身是項乏味的工作。雖然如此,其巨大好處讓許多新舊技術紛紛開始使用非同步編程。微軟自發布了 .NET 4.0以後也對其投入頗多,隨後在 .NET 4.5中引入了 async 和 await 關鍵字,使非同步編程變得前所未有地簡單。
但是,ASP.NET 中的非同步功能自一開始就可以使用,只是從來沒有得到應有的重視。而且,考慮到 ASP.NET 和 IIS 處理請求的方式,非同步體現的優勢可能更明顯。通過非同步,我們很容易就可以大幅提高 ASP.NET 應用程式的擴充性。隨著新的編程結構引入,如 async 和 await 關鍵字,我們也應該學會使用非同步編程的強大功能。
在本篇博文中,我們將討論一下 IIS 和 ASP.NET 處理請求的方式,然後看看 ASP.NET 中哪些地方可以使用非同步,最後再討論幾個最能體現非同步優勢的情境。
請求是如何處理的?
每個 ASP.NET 請求都要先通過 IIS,然後再由 ASP.NET 處理常式進行最終處理。 首先IIS 接收請求,初步處理後,發送給ASP.NET(必須是一個ASP.NET請求),然後由ASP.NET進行實際處理並產生響應,之後該響應通過IIS發回給客戶。在IIS上,有一些背景工作處理序負責從隊列中取出請求,並執行IIS 模組,然後再將該請求發送到ASP.NET 隊列。但是,ASP.NET本身不建立任何線程,也沒有處理請求的線程池,而是通過使用CLR 線程池,從中擷取線程來處理請求。因此,IIS 模組調用ThreadPool.QueueUserWorkItem,將請求排入隊列,供CLR 背景工作執行緒處理。我們都知道,CLR線程池是由CLR管理,並且能夠自動調整(也就是說,它根據需要建立和銷毀進程)。這裡還要記住,建立和銷毀線程是項很繁重的任務,這就是為什麼CLR線程池允許使用同一個線程處理多個任務。下面來看一個描述請求處理過程的圖示。
在中可以看到,請求首先由 HTTP.sys接收,並添加到相應核心級應用程式集區隊列。然後,一個IIS背景工作執行緒從隊列中取出請求,處理後將其傳到ASP.NET 隊列。注意,該請求如果不是一個ASP.NET請求,將從 IIS 自動返回。最後,從CLR線程池中分配一個線程,負責處理該請求。
ASP.NET中非同步使用情境是?
所有請求大致可以分為兩類:
CPU Bound 類
I/O Bound 類
CPU Bound 類請求,需要 CPU 時間,而且是在同一進程中執行;而 I/O Bound 類請求,本身具有阻塞性,需要依賴其他模組執行 I/O 操作並返迴響應。阻塞性請求是提高應用程式延展性的主要障礙,而且大多數web應用程式中,在等待 I/O 操作的過程中浪費了大量時間。 因此以下情境適合使用非同步:
I/O Bound 類請求,包括:
資料庫訪問
讀/寫檔案
Web 服務調用
訪問網路資源
事件驅動的請求,比如SignalR
需要從多個資料來源擷取資料的情境
作為樣本,這裡建立一個簡單的同步頁面,然後再將它轉換成非同步頁面。 本樣本設定了1000ms的延遲(以類比一些繁重的資料庫或web服務調用等),而且還使用WebClient下載了一個頁面,如下所示:
protected void Page_Load(object sender, EventArgs e) { System.Threading.Thread.Sleep(1000); WebClient client = new WebClient(); string downloadedContent = client.DownloadString("https://msdn.microsoft.com/en-us/library/hh873175%28v=vs.110%29.aspx"); dvcontainer.InnerHtml = downloadedContent; }
現在將該頁面轉換成非同步頁面,這裡主要涉及三個步驟:
在頁面指令中添加Async = true,將該頁面轉換成非同步頁面,如下所示:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Home.aspx.cs" Inherits="AsyncTest.Home" Async="true" AsyncTimeout="3000" %>
這裡還添加了 AsyncTimeout (可選項),請根據需求選擇。
2.將此方法轉換成非同步方法呼叫。在這裡把Thread.Sleep 與 client.DownloadString 轉換成非同步方法呼叫如下所示:
private async Task AsyncWork() { await Task.Delay(1000); WebClient client = new WebClient(); string downloadedContent = await client.DownloadStringTaskAsync("https://msdn.microsoft.com/en-us/library/hh873175%28v=vs.110%29.aspx "); dvcontainer.InnerHtml = downloadedContent; }
3.現在可以直接在 Page_Load (頁面載入)上調用此方法,使其非同步,如下所示:
protected async void Page_Load(object sender, EventArgs e) { await AsyncWork(); }
但是這裡的 Page_Load 返回的類型是async void,這種情況無論如何都應該避免。我們知道,Page_Load 是整個頁面生命週期的一部分,如果我們把它設定成非同步,可能會出現一些異常情況和事件,比如生命週期已經執行完畢而頁面載入仍在運行。 因此,強烈建議大家使用 RegisterAsyncTask 方法註冊非同步任務,這些非同步任務會在生命週期的恰當時間執行,可以避免出現任何問題。
protected void Page_Load(object sender, EventArgs e) { RegisterAsyncTask(new PageAsyncTask(AsyncWork)); }
現在,頁面已經轉換成了非同步頁,它就不再是一個阻塞性請求。
筆者在 IIS8.5 上部署了同步頁面和非同步頁面,並使用突發負載對兩者進行了測試。測試結果發現,相同的機器配置,同步頁面在2-3秒內只能提取1000個請求,而非同步頁面能夠為2200多個請求提供服務。此後,開始收到逾時(Timeout)或伺服器不可用(Server Not Available)的錯誤。雖然兩者的平均請求處理時間沒有多大差別,但是通過非同步頁面,可以處理兩倍以上的請求。這足以證明非同步編程功能強大,所以應該充分利用它的優勢。
ASP.NET中還有幾個地方也可以引入非同步:
編寫非同步模組
使用IHttpAsyncHandler 或 HttpTaskAsyncHandler 編寫非同步HTTP處理常式
使用web sockets 或 SignalR
結論
本篇博文中,我們討論了非同步編程,而且發現,新推出的async 和 await關鍵字,使非同步編程變得十分簡單。我們討論的話題包括 IIS和ASP.NET如何處理請求,以及在哪些情境中非同步作用最明顯。另外,我們還建立了一個簡單樣本,討論了非同步頁面的優勢。最後我們還補充了幾個ASP.NET中可以使用非同步地方。