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