ASP.NET中的非同步編程
為什麼要在ASP.NET中使用非同步編程?
ASP.NET 使用公用語言運行庫 (CLR) 線程池中的線程來處理請求。只要線上程池中存在可用線程,ASP.NET 調度傳入請求就不會有任何麻煩。但是一旦線程池處於飽和狀態(即所有池中的線程忙於處理請求,而沒有可用的線程),則新的請求必須等待線程可用。如果這種僵局變得相當嚴重、隊列到達容量限制,ASP.NET 將束手無策,對於新的請求Web 服務器會拒絕並返回 HTTP 503 狀態(伺服器太忙)。
一種解決方案是提高線程池的上限,以建立更多的線程。這是當其客戶報告頻繁遇到“伺服器不可用”錯誤時,開發人員經常採取的方法。另一種經常採用的方法是放棄出現問題的硬體,向 Web 場中添加更多的伺服器。但是,增加線程數或伺服器數並不能從根本上解決這一問題。
真正可擴充的 ASP.NET 網站應該充分利用線程池。如果由於所有線程都在消耗 CPU 而造成線程池飽和,除了添加伺服器,您幾乎無計可施。然而,多數 Web 應用程式可以與資料庫、Web 服務或其他外部實體進行通話,並通過強制線程池等待完成資料庫查詢、Web 服務調用和其他 I/O 操作來限制可擴充性。針對資料驅動的網頁的查詢可能要花費千分之幾秒來執行代碼,花幾秒鐘等待資料庫查詢返回。當查詢未完成時,分配給請求的線程無法服務於其他的請求。這就是所謂的玻璃屋頂。如果您要構建具有高度可擴充性的網站,這種情況是您必須避免的。請記住:當涉及輸送量時,除非處理得當,否則
I/O 會成為大問題。
所以,為了提高網站的輸送量和擴充性應該使用非同步編程來進行IO方面的操作。同時,我們也應該知道,使用非同步IO編程並不能縮短一次IO操作的時間,所以,並不能使使用者感覺到網站的速度快了。但是,如果一個頁面需要依次進行多次IO操作才能完成,這時候對每個IO都進行非同步作業,這時候頁面操作完成的時間將是多個IO操作中耗時最長的那個時間,而不是多個IO耗時的總和。這時候,使用者將徹底感覺到網站快了。
如何選擇使用同步操作還是非同步作業?
這隻是一些準則;您必須逐個檢查每個應用程式以確定非同步作業方法是否能協助提高效能。
通常,在滿足以下條件時使用同步操作:
1、操作很簡單或已耗用時間很短。
2、簡單性比效率更重要。
3、此操作主要是 CPU 操作而不是包含大量的磁碟或網路開銷的操作。 對 CPU 綁定操作使用非同步作業方法未提供任何好處並且還因為頻繁的線程間切換導致更多的系統開銷。
通常,在滿足以下條件時使用非同步作業:
1、操作是網路綁定或 I/O 綁定而不是 CPU 綁定的。
2、測試顯示阻塞操作對於網站效能是一個瓶頸,並且通過對這些阻塞調用使用非同步作業方法,IIS 可對更多的請求提供服務。
3、並行性比代碼的簡單性更重要。
4、您希望提供一種可讓使用者取消長時間啟動並執行請求的機制。
在ASP.NET中如何使用非同步編程?
在ASP.NET中使用非同步編程的方式有三種:
一、非同步頁面
如果您有一些頁面要執行相對較長的 I/O 操作,它們就應成為非同步頁面。如果某頁面查詢資料庫,花了 5 秒鐘返回(因為它既返回大量資料,又通過大量載入的串連將目標鎖定到遠端資料庫),線程分配給該請求的 5 秒鐘不可用於其他請求。如果每個請求都照此處理,應用程式將會很快陷入停頓。
而使用非同步頁面的時候,請求到達後,由 ASP.NET 為其分配一個線程。該請求開始在該線程中進行處理,當選擇資料庫時,請求將啟動非同步 ADO.NET 查詢,並將線程返回到線程池中。當查詢完成時,ADO.NET 回調到 ASP.NET,ASP.NET 從線程池中調出另一個線程,並恢複處理請求。
使用非同步頁面的步驟為:
1、設定ASPX 的 Page 指令中的 Async="true" 屬性。
2、在Page_Load 方法調用 AddOnPreRenderCompleteAsync或RegisterAsyncTask 方法以註冊開始和結束處理常式。
3、非同步呼叫資料庫查詢(需要在連接字串中設定Async="true")、檔案操作(使用FileOptions.Asynchronous屬性開啟檔案流)或者WebService。
以下為使用AddOnPreRenderCompleteAsync方法的非同步頁面的樣本
using System;using System.Data;using System.Data.SqlClient;using System.Web;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.Configuration;public partial class AsyncDataBind : System.Web.UI.Page{ private SqlConnection _connection; private SqlCommand _command; private SqlDataReader _reader; protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { AddOnPreRenderCompleteAsync( new BeginEventHandler(BeginAsyncOperation), new EndEventHandler(EndAsyncOperation) ); } } IAsyncResult BeginAsyncOperation(object sender, EventArgs e, AsyncCallback cb, object state) { string connect = WebConfigurationManager.ConnectionStrings ["AsyncPubs"].ConnectionString; _connection = new SqlConnection(connect); _connection.Open(); _command = new SqlCommand( "SELECT title_id, title, price FROM titles", _connection); //原理說明:在此處cb指向的函數為Void OnAsyncHandlerCompletion(System.IAsyncResult), // 該函數會在BeginExecuteReader非同步呼叫完成後調用,在該函數中會執行在Page_Load // 中註冊的EndEventHandler委託(在這裡就是EndAsyncOperation函數) return _command.BeginExecuteReader(cb, state); } void EndAsyncOperation(IAsyncResult ar) { _reader = _command.EndExecuteReader(ar); } protected void Page_PreRenderComplete(object sender, EventArgs e) { Output.DataSource = _reader; Output.DataBind(); } public override void Dispose() { if (_connection != null) _connection.Close(); base.Dispose(); }}
二、非同步HTTP處理常式
通過編寫自訂 HTTP 處理常式,您可以擴充 ASP.NET 以支援其他檔案類型。但是,更有趣的一點是,您可以在 ASHX 檔案中部署 HTTP 處理常式,並將它們用作 HTTP 要求的目標。這是構建動態產生映像或從資料庫中檢索映像的 Web 端點的正確方法。您只需將 <img> 標記(或 Image 控制項)包含在頁面中,並將其指向建立或擷取映像的 ASHX。將目標鎖定到帶有請求的 ASHX 檔案比將目標鎖定到 ASPX 檔案更有效,因為 ASHX 檔案在處理時開銷更少。
在非同步 HTTP 處理常式的處理過程中,ASP.NET 將通常用於外部進程的線程放回線程池中,直到處理常式接收到來自外部進程的回調。由於只能同時執行有限數量的線程,因此這樣可以避免阻止線程並改善效能。如果許多使用者都在請求依賴於外部進程的同步 HTTP 處理常式,那麼作業系統可能很快就會用完所有線程,因為大量線程被阻止,正在等待外部進程。
下邊給出一個使用非同步讀取圖片檔案的非同步Http處理常式的樣本
using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.IO;namespace TestWeb{ public class SearchImageAsync:IHttpAsyncHandler { private string imgPath = "images/bg.jpg"; private const int BUFFER_SIZE = 1024; public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData) { FileStream fs = new FileStream(context.Server.MapPath(imgPath), FileMode.Open, FileAccess.Read, FileShare.Read, BUFFER_SIZE, FileOptions.Asynchronous); byte[] buffer = new byte[BUFFER_SIZE]; MemoryStream result = new MemoryStream(); return fs.BeginRead(buffer, 0, BUFFER_SIZE, new AsyncCallback(ReadFileCB), new ReadFileStateInfo(context, cb, extraData, fs, buffer, result)); } private void ReadFileCB(IAsyncResult ar) { ReadFileStateInfo state = ar.AsyncState as ReadFileStateInfo; if(state == null) return; FileStream fs = state.ReadFileStream; int count = fs.EndRead(ar); if (count > 0) { state.ResultStream.Write(state.ReadBuffer, 0, count); fs.BeginRead(state.ReadBuffer, 0, BUFFER_SIZE, new AsyncCallback(ReadFileCB), state); } else { if (fs != null) { fs.Close(); fs = null; } state.EndProcessCB(ar); } } public void EndProcessRequest(IAsyncResult result) { ReadFileStateInfo state = result.AsyncState as ReadFileStateInfo; if (state == null) return; state.Context.Response.ContentType = "image/*"; state.Context.Response.BinaryWrite(state.ResultStream.ToArray()); state.ResultStream.Close(); } public bool IsReusable { get { return false; } } public void ProcessRequest(HttpContext context) { throw new NotImplementedException(); } } public class ReadFileStateInfo { private AsyncCallback mEndProcessCB; public AsyncCallback EndProcessCB { get { return this.mEndProcessCB; } set { this.mEndProcessCB = value; } } private object mAsyncExtraData; public object AsyncExtraData { get { return this.mAsyncExtraData; } set { this.mAsyncExtraData = value; } } private FileStream mReadFileStream; public FileStream ReadFileStream { get { return this.mReadFileStream; } set { this.mReadFileStream = value; } } private byte[] mReadBuffer; /// <summary> /// 讀取檔案的Buffer /// </summary> public byte[] ReadBuffer { get { return this.mReadBuffer; } set { this.mReadBuffer = value; } } private MemoryStream mResultStream; /// <summary> /// 儲存結果的記憶體流 /// </summary> public MemoryStream ResultStream { get { return this.mResultStream; } set { this.mResultStream = value; } } private HttpContext mContext; public HttpContext Context { get { return this.mContext; } set { this.mContext = value; } } public ReadFileStateInfo(HttpContext context, AsyncCallback cb, object extraData, FileStream readFileStream, byte[] readBuffer, MemoryStream result) { this.mContext = context; this.mEndProcessCB = cb; this.mAsyncExtraData = extraData; this.mReadFileStream = readFileStream; this.mReadBuffer = readBuffer; this.mResultStream = result; } }}
在VS2010中註冊該Http處理常式(IIS7.0的傳統模式)
<configuration> <system.webServer> <handlers> <remove name="ChartImageHandler" /> <add verb="*" path="*.ImgAsync" name="SearchImageAsync" type="TestWeb.SearchImageAsync" /> </handlers> </system.webServer> <system.web> <httpHandlers> <add verb="*" path="*.ImgAsync" type="TestWeb.SearchImageAsync"/> </httpHandlers> </system.web></configuration>
現在通過副檔名*.ImgAsync就能開啟圖片了(讀取圖片只是為了說明該非同步作業方法)。
三、非同步HTTP模組
HTTP 模組是位於 ASP.NET 管道中的對象,在管道中,它可以查看甚至修改傳入請求和傳出響應。ASP.NET 中的許多主要服務都是以 HTTP 模組的形式實現的,包括身分識別驗證、授權和輸出緩衝。通過編寫自訂 HTTP 模組並將它們插入管道,您可以擴充 ASP.NET。當您這樣做的時候,一定要認真考慮這些 HTTP 模組是否應當是非同步。
要使用非同步Http模組,可以在IHttpModule的Init方法中,調用application.AddOnPreRequestHandlerExecuteAsync方法註冊兩個回呼函數來實現。
下面是一個記錄使用者訪問Log的HttpModule的樣本。
using System;using System.Web;using System.IO;using System.Threading;using System.Text;namespace TestWeb{ public class AsyncRequestLogModule : IHttpModule { private FileStream _file; private static long _position = 0; private static object _lock = new object(); public void Init(HttpApplication application) { application.AddOnPreRequestHandlerExecuteAsync( new BeginEventHandler(BeginPreRequestHandlerExecute), new EndEventHandler(EndPreRequestHandlerExecute) ); } IAsyncResult BeginPreRequestHandlerExecute(Object source, EventArgs e, AsyncCallback cb, Object state) { HttpApplication app = (HttpApplication)source; DateTime time = DateTime.Now; string line = String.Format( "{0,10:d} {1,11:T} {2, 32} {3}\r\n", time, time, app.User.Identity.IsAuthenticated ? app.User.Identity.Name : app.Request.UserHostAddress, app.Request.Url); byte[] output = Encoding.ASCII.GetBytes(line); lock (_lock) { _file = new FileStream( HttpContext.Current.Server.MapPath( "~/App_Data/RequestLog.txt"), FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write, 1024, true); _file.Seek(_position, SeekOrigin.Begin); _position += output.Length; return _file.BeginWrite(output, 0, output.Length, cb, state); } } void EndPreRequestHandlerExecute(IAsyncResult ar) { _file.EndWrite(ar); _file.Close(); } public void Dispose() { } }}
在web.config檔案中註冊該HttpModule。
<configuration><system.web> <httpModules> <add name="AsyncRequestLogModule" type="TestWeb.AsyncRequestLogModule"/> </httpModules></system.web></configuration>
參考:http://msdn.microsoft.com/zh-cn/magazine/cc163463.aspx
http://www.cnblogs.com/flier/archive/2005/12/27/305233.aspx