ASP
NET
提供了大量新功能
其中包括声明性数据绑定和母版页
成员和角色管理服务等
但我认为最棒的功能是异步页
接下来让我告诉您其中的原因
当 ASPNET 接收针对页的请求时它从线程池中提取一个线程并将请求分配给该线程一个普通的(或同步的)页在该请求期间保留线程从而防止该线程用于处理其他请求如果一个同步请求成为 I/O 绑定(例如如果它调用一个远程 Web 服务或查询一个远程数据库并等待调用返回)那么分配给该请求的线程在调用返回之前处于挂起状态这影响了可伸缩性原因是线程池的可用线程是有限的如果所有请求处理线程全部阻塞以等待 I/O 操作完成则其他请求排入队列等待线程释放最好的情况是吞吐量减少因为请求等待较长的时间才能得到处理最坏的情况则是该队列填满并且 ASPNET 因 Server Unavailable错误使后续请求失败
异步页为由 I/O 绑定的请求引起的问题提供优秀的解决方案页处理从线程池线程开始但是当一个异步 I/O 操作开始响应 ASPNET 的信号之后该线程返回线程池当该操作完成时ASPNET 从线程池提取另一个线程并完成该请求的处理由于线程池线程得到了更高效的使用因此提高了可伸缩性那些挂起等待 I/O 完成的线程现在可用于服务其他请求直接的受益方是不执行长时间 I/O 操作并因此可以快速进出管线的请求长时间等待进入管线会对此类请求的性能带来不小的负面影响
ASPNET Beta 异步页基础结构的相关文档很少让我们展望一下异步页的前景从而弥补这点不足请记住本专栏涉及 ASPNET 和 NET Framework 的测试版本
ASPNET x 中的异步页
ASPNET x 本质上不支持异步页但是通过坚韧的努力和不懈地创新可以生成异步页有关更多概述信息请参阅相关资料
这里的技巧是在一个页的代码隐藏类中实现 IhttpAsyncHandler从而提示 ASPNET 通过调用 IHttpAsyncHandlerBeginProcessRequest 来处理请求而不是通过调用该页的 IHttpHandlerProcessRequest 方法然后您的 BeginProcessRequest 实现可以启动另一个线程该线程调用 baseProcessRequest使得页进入其常规请求处理生命周期(完成诸如 Load 和 Render 的事件)但是在非 ThreadPool 线程上例外同时启动新线程之后 BeginProcessRequest 立即返回从而允许执行 BeginProcessRequest 的线程返回线程池
这是基本思想但细节中还有很多注意事项其中您需要实现 IAsyncResult并从 BeginProcessRequest 中返回它这通常意味着创建一个 ManualResetEvent 对象并且当 ProcessRequest 在后台线程中返回时向其发送信号此外您必须提供调用 baseProcessRequest 的线程遗憾的是多数用于将工作移到后台线程的常规技术(包括 ThreadStartThreadPoolQueueUserWorkItem 和异步委托)在 ASPNET 应用程序中都是起反作用的因为它们或者从线程池偷盗线程或者有不受限制的线程增长的危险正确的异步页实现使用自定义线程池但自定义线程池类不容易编写
主要是在 ASPNET x 中生成异步页并非不可能而是有些乏味在尝试一两次之后您不禁会想一定会有更好的方法目前这个好方法就是 ASPNET
ASPNET 中的异步页
ASPNET 极大地简化了生成异步页的方式首先使用该页的 @ Page 指令引入 Async=true 属性如下所示
在后台这会通知 ASPNET 在该页中实现 IhttpAsyncHandler接下来您在该页生存期的早期(例如在 Page_Load 时)调用新的 PageAddOnPreRenderCompleteAsync 方法来注册一个 Begin 方法和一个 End 方法
如以下代码所示
AddOnPreRenderCompleteAsync ( new BeginEventHandler(MyBeginMethod) new EndEventHandler (MyEndMethod) );
接下来的操作比较有趣该页经历其常规处理生命周期直到 PreRender 事件刚刚引发之后然后ASPNET 调用使用 AddOnPreRenderCompleteAsync 注册的 Begin 方法Begin 方法的任务是启动诸如数据库查询或 Web 服务调用的异步操作并立即返回此时分配给该请求的线程返回到线程池此外Begin 方法返回 IAsyncResult它允许 ASPNET 确定异步操作完成的时间这个时候 ASPNET 从线程池提取线程并调用 End 方法当 End 返回之后ASPNET 执行该页生命周期其余的部分包括呈现阶段在 Begin 返回以及调用 End 之间该请求处理线程可以自由地服务于其他请求直至调用 End 且延迟呈现为止由于 版的 NET Framework 提供多种执行异步操作的方式因此您甚至无需实现 IasyncResult反之Framework 替您实现
图 中的代码隐藏类提供一个示例响应页包含一个 ID 为Output的 Label 控件该页使用 SystemNetHttpWebRequest 类提取 的内容然后它分析返回的 HTML并将它发现的全部 HREF 目标列表写出到 Label 控件
图
using System;
using SystemWeb;
using SystemWebUI;
using SystemWebUIWebControls;
using SystemNet;
using SystemIO;
using SystemText;
using SystemTextRegularExpressions;
public partial class AsyncPage : SystemWebUIPage
{
private WebRequest _request;
void Page_Load (object sender EventArgs e)
{
AddOnPreRenderCompleteAsync (
new BeginEventHandler(BeginAsyncOperation)
new EndEventHandler (EndAsyncOperation)
);
}
IAsyncResult BeginAsyncOperation (object sender EventArgs e
AsyncCallback cb object state)
{
_request = WebRequestCreate();
return _requestBeginGetResponse (cb state);
}
void EndAsyncOperation (IAsyncResult ar)
{
string text;
using (WebResponse response = _requestEndGetResponse(ar))
{
using (StreamReader reader = new StreamReader(responseGetResponseStream()))
{
text = readerReadToEnd();
}
}
Regex regex = new Regex (href\\s*=\\s*\([^\]*)\ RegexOptionsIgnoreCase);
MatchCollection matches = regexMatches(text);
StringBuilder builder = new StringBuilder();
foreach (Match match in matches)
{
builderAppend (matchGroups[]);
builderAppend(
);
}
OutputText = builderToString ();
}
}
由于 HTTP 请求需要较长时间才能返回因此AsyncPageaspxcs 异步执行对它的处理它在 Page_Load 中注册 Begin 和 End 方法并且在 Begin 方法中它调用 HttpWebRequestBeginGetResponse 启用一个异步 HTTP 请求BeginAsyncOperation 将由 BeginGetResponse 返回的 IAsyncResult 返回到 ASPNET导致当 HTTP 请求完成时ASPNET 调用 EndAsyncOperationEndAsyncOperation 进而分析该内容并将结果写入 Label 控件之后进行呈现并且 HTTP 响应返回到浏览器
图 同步和异步页处理
图 说明 ASPNET 同步和异步页之间的区别当请求同步页时ASPNET 为该请求分配线程池中的一个线程并在该线程上执行页如果该请求停止执行 I/O 操作则挂起线程直到完成操作从而可以完成该页的生命周期相反异步页通常通过 PreRender 事件执行然后调用使用 AddOnPreRenderCompleteAsync 注册的 Begin 方法之后该请求处理线程返回线程池Begin 启动一个异步 I/O 操作当该操作完成时ASPNET 从线程池提取另一个线程并调用 End 方法并且在该线程上执行该页生命周期的其余部分
图 跟蹤输出显示异步页的异步点
对 Begin 的调用标记该页的异步点图 中的跟蹤准确显示异步点发生在何处如果调用则必须在异步点之前调用 AddOnPreRenderCompleteAsync — 即不晚于该页的 PreRender 事件
异步数据绑定
通常情况下ASPNET 页并不使用 HttpWebRequest 直接请求其他页但它们通常查询数据库并对结果进行数据绑定因此您将如何使用异步页执行异步数据绑定呢?图 中的代码隐藏类显示进行此操作的一种方式
using System;
using SystemData;
using SystemDataSqlClient;
using SystemWeb;
using SystemWebUI;
using SystemWebUIWebControls;
using SystemWebConfiguration;
public partial class AsyncDataBind : SystemWebUIPage
{
private SqlConnection _connection;
private SqlCommand _command;
private SqlDataReader _reader;
protected void Page_Load(object sender EventArgs e)
{
if (!IsPostBack)
{
// Hook PreRenderComplete event for data binding
thisPreRenderComplete += new EventHandler(Page_PreRenderComplete);
// Register async methods
AddOnPreRenderCompleteAsync(
new BeginEventHandler(BeginAsyncOperation)
new EndEventHandler(EndAsyncOperation)
);
}
}
IAsyncResult BeginAsyncOperation (object sender EventArgs e AsyncCallback cb object state)
{
string connect = WebConfigurationManagerConnectionStrings
[PubsConnectionString]ConnectionString;
_connection = new SqlConnection(connect);
_connectionOpen();
_command = new SqlCommand(SELECT title_id title price FROM titles _connection);
return _commandBeginExecuteReader (cb state);
}
void EndAsyncOperation(IAsyncResult ar)
{
_reader = _commandEndExecuteReader(ar);
}
protected void Page_PreRenderComplete(object sender EventArgs e)
{
OutputDataSource = _reader;
OutputDataBind();
}
public override void Dispose()
{
if (_connection != null) _connectionClose();
baseDispose();
}
}
AsyncDataBindaspxcs 与 AsyncPageaspxcs 使用相同的 AddOnPreRenderCompleteAsync 模式但是AsyncDataBindaspxcs 的 BeginAsyncOperation 方法调用 ADONET 中的新方法 SqlCommandBeginExecuteReader(而非 HttpWebRequestBeginGetResponse)以执行一个异步数据库查询当调用完成时EndAsyncOperation 调用 SqlCommandEndExecuteReader 以获取 SqlDataReader然后将其存储在私有字段中在用于 PreRenderComplete 事件(在异步操作完成但呈现该页之前引发)的事件处理程序中AsyncDataBindaspxcs 之后将 SqlDataReader 绑定到 Output GridView 控件从外观上看该页类似于使用 GridView 呈现数据库查询结果的普通(同步)页但是在内部该页更具可伸缩性因为它并不挂起线程池线程以等待查询返回
异步调用 Web 服务
另一个通常由 ASPNET Web 页执行的
与 I/O 相关的任务是调出 Web 服务由于 Web 服务调用花费较长时间才能返回因此执行它们的页是用于异步处理的理想选择
图
using System;
using SystemData;
using SystemConfiguration;
using SystemWeb;
using SystemWebUI;
using SystemWebUIWebControls;
public partial class AsyncWSInvoke : SystemWebUIPage
{
private WSPubsWebService _ws;
private DataSet _ds;
protected void Page_Load(object sender EventArgs e)
{
if (!IsPostBack)
{
// Hook PreRenderComplete event for data binding
thisPreRenderComplete += new EventHandler(Page_PreRenderComplete);
// Register async methods
AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginAsyncOperation)
new EndEventHandler(EndAsyncOperation)
);
}
}
IAsyncResult BeginAsyncOperation (object sender EventArgs e AsyncCallback cb object state)
{
_ws = new WSPubsWebService();
// Fix up URL for call to local VWDhosted Web service
_wsUrl = new Uri(RequestUrl Pubsasmx)ToString();
_wsUseDefaultCredentials = true;
return _wsBeginGetTitles (cb state);
}
void EndAsyncOperation(IAsyncResult ar)
{
_ds = _wsEndGetTitles(ar);
}
protected void Page_PreRenderComplete(object sender EventArgs e)
{
OutputDataSource = _ds;
OutputDataBind();
}
public override void Dispose()
{
if (_ws != null) _wsDispose();
baseDispose();
}
}
图 显示生成调出 Web 服务的异步页的方式它使用图 和 图 中相同的 AddOnPreRenderCompleteAsync 机制该页的 Begin 方法通过调用 Web 服务代理的异步 Begin 方法启动一个异步 Web 服务调用该页的 End 方法在私有字段中缓存对 Web 方法返回的 DataSet 的引用并且 PreRenderComplete 处理程序将 DataSet 绑定到 GridView作为参考该调用的目标 Web 方法如以下代码所示
[WebMethod] public DataSet GetTitles () { string connect = WebConfigurationManagerConnectionStrings [PubsConnectionString]ConnectionString; SqlDataAdapter adapter = new SqlDataAdapter (SELECT title_id title price FROM titles connect); DataSet ds = new DataSet(); adapterFill(ds); return ds; }
这只是其中一种方式但并不是唯一的方式NET Framework Web 服务代理支持两种对 Web 服务进行异步调用的机制一个是 NET Framework x 和 Web 服务代理中的每方法 Begin 和 End 方法另一个是仅由 NET Framework 的 Web 服务代理提供的新 MethodAsync 方法和 MethodCompleted 事件
如果一个 Web 服务有一个名为 Foo 的方法那么除了具有名为 FooBeginFoo 和 EndFoo 的方法外NET Framework 版本 Web 服务代理还包括名为 FooAsync 的方法和名为 FooCompleted 的事件可以通过注册 FooCompleted 事件的处理程序并调用 FooAsync 来异步调用 Foo如下所示
proxyFooCompleted += new FooCompletedEventHandler (OnFooCompleted); proxyFooAsync (); void OnFooCompleted (Object source FooCompletedEventArgs e) { // Called when Foo completes }
当异步调用由于 FooAsync 完成而开始时将引发 FooCompleted 事件从而导致调用 FooCompleted 事件处理程序包装该事件处理程序 (FooCompletedEventHandler) 的委托和传递给它的第二个参数 (FooCompletedEventArgs) 都随 Web 服务代理一起生成可通过 FooCompletedEventArgsResult 访问 Foo 的返回值
图 展示使用 MethodAsync 模式异步调用 Web 服务的 GetTitles 方法的代码隐藏类从功能上讲该页等同于图 中的页但其内部实现则大为不同AsyncWSInvokeaspx 包括一个 @ Page Async=true 指令类似于 AsyncWSInvokeaspx但是AsyncWSInvokeaspxcs 并不调用 AddOnPreRenderCompleteAsync它注册一个用于 GetTitlesCompleted 事件的处理程序并调用 Web 服务代理上的 GetTitlesAsyncASPNET 仍然延迟呈现该页直到 GetTitlesAsync 完成在内部当异步调用开始以及完成时它使用 SystemThreadingSynchronizationContext 的一个实例( 的一个新类)接收通知
图
using System;
using SystemData;
using SystemConfiguration;
using SystemWeb;
using SystemWebUI;
using SystemWebUIWebControls;
public partial class AsyncWSInvoke : SystemWebUIPage
{
private WSPubsWebService _ws;
private DataSet _ds;
protected void Page_Load(object sender EventArgs e)
{
if (!IsPostBack)
{
// Hook PreRenderComplete event for data binding
thisPreRenderComplete += new EventHandler(Page_PreRenderComplete);
// Call the Web service asynchronously
_ws = new WSPubsWebService();
_wsGetTitlesCompleted += new
WSGetTitlesCompletedEventHandler(GetTitlesCompleted);
_wsUrl = new Uri(RequestUrl Pubsasmx)ToString();
_wsUseDefaultCredentials = true;
_wsGetTitlesAsync();
}
}
void GetTitlesCompleted(Object source
WSGetTitlesCompletedEventArgs e)
{
_ds = eResult;
}
protected void Page_PreRenderComplete(object sender EventArgs e)
{
OutputDataSource = _ds;
OutputDataBind();
}
public override void Dispose()
{
if (_ws != null) _wsDispose();
baseDispose();
}
}
使用 MethodAsync 而非 AddOnPreRenderCompleteAsync 实现异步页有两个优势首先MethodAsync 将模拟区域性和 HttpContextCurrent 注入 MethodCompleted 事件处理程序而 AddOnPreRenderCompleteAsync 则不然其次如果该页进行多个异步调用而且必须延迟呈现直到所有调用完成则使用 AddOnPreRenderCompleteAsync 要求您生成一个在所有调用完成前保持无信号状态的 IasyncResult使用 MethodAsync这样的操作就不是必需的您只需放置这些调用(数量不限)ASPNET 引擎延迟该呈现阶段直到最后一个调用返回
异步任务
MethodAsync 是从异步页进行多个异步 Web 服务调用并延迟呈现阶段直到所有调用完成的一个简便方法但如果您想在一个异步页中执行若干异步 I/O 操作而且这些操作不涉及 Web 服务那该如何呢? 这么说可以反过来生成一个 IAsyncResult它可以返回到 ASPNET 以允许它了解最后一个调用何时完成的吗? 幸运的是答案是否定的
在 ASPNET 中SystemWebUIPage 类引入了另一个方法来简化异步操作 RegisterAsyncTaskRegisterAsyncTask 比 AddOnPreRenderCompleteAsync 具有四个优势首先除了 Begin 和 End 方法RegisterAsyncTask 还允许您注册当异步操作长时间无法完成时调用的超时方法您可以通过在该页的 @ Page 指令中包含 AsyncTimeout 属性以声明性方式设置超时AsyncTimeout= 将超时设置为 秒第二个优势是您可以在一个请求中多次调用 RegisterAsyncTask 来注册若干异步操作和使用 MethodAsync 一样ASPNET 延迟呈现该页直到所有操作完成第三您可以使用 RegisterAsyncTask 的第四个参数将状态传递给 Begin 方法最后RegisterAsyncTask 将模拟区域性和 HttpContextCurrent 注入 End 和 Timeout 方法正如本文前面提到的使用 AddOnPreRenderCompleteAsync 注册的 End 方法的情况则不然
在其他方面依赖于 RegisterAsyncTask 的异步页与依赖于 AddOnPreRenderCompleteAsync 的异步页相类似它仍然需要 @ Page 指令(或等效的编程指令它会将该页的 AsyncMode 属性设置为 true)中的 Async=true 属性而且它仍然与平时一样通过 PreRender 事件执行此时调用使用 RegisterAsyncTask 注册的 Begin 方法而且进一步保持请求处理直到最后一个操作完成
例如图 中的代码隐藏类在功能上与图 中的等效但是它使用 RegisterTaskAsync 而非使用 AddOnPreRenderCompleteAsync请注意名为 TimeoutAsyncOperation 的超时处理程序如果 HttpWebRequestBeginGetRequest 长时间无法完成将调用该处理程序相应的 aspx 文件包括一个将超时间隔设置为 秒的 AsyncTimeout 属性还请注意传给 RegisterAsyncTask 的第四个参数(可用于将数据传送到 Begin 方法)的 null
图
using System;
using SystemWeb;
using SystemWebUI;
using SystemWebUIWebControls;
using SystemNet;
using SystemIO;
using SystemText;
using SystemTextRegularExpressions;
public partial class AsyncPageTask : SystemWebUIPage
{
private WebRequest _request;
protected void Page_Load(object sender EventArgs e)
{
PageAsyncTask task = new PageAsyncTask(
new BeginEventHandler(BeginAsyncOperation)
new EndEventHandler(EndAsyncOperation)
new EndEventHandler(TimeoutAsyncOperation)
null
);
RegisterAsyncTask(task);
}
IAsyncResult BeginAsyncOperation(object sender EventArgs e
AsyncCallback cb object state)
{
_request = WebRequestCreate();
return _requestBeginGetResponse(cb state);
}
void EndAsyncOperation(IAsyncResult ar)
{
string text;
using (WebResponse response = _requestEndGetResponse(ar))
{
using (StreamReader reader = new StreamReader(responseGetResponseStream()))
{
text = readerReadToEnd();
}
}
Regex regex = new Regex(href\\s*=\\s*\([^\]*)\ RegexOptionsIgnoreCase);
MatchCollection matches = regexMatches(text);
StringBuilder builder = new StringBuilder();
foreach (Match match in matches)
{
builderAppend(matchGroups[]);
builderAppend(
);
}
OutputText = builderToString();
}
void TimeoutAsyncOperation(IAsyncResult ar)
{
OutputText = Data temporarily unavailable;
}
}
RegisterAsyncTask 的主要优势在于它允许异步页引发多个异步调用并延迟呈现直到所有调用完成它也很好地适用于单个异步调用而且它提供了 AddOnPreRenderCompleteAsync 不具有的超时选项如果生成一个只进行一个异步调用的异步页您可以使用 AddOnPreRenderCompleteAsync 或 RegisterAsyncTask但对于放置两个以上异步调用的异步页RegisterAsyncTask 极大地简化了您的操作
由于超时值是每页而非每调用设置因此您可能想知道是否能改变单个调用的超时值简单的回答是否您可以通过以编程方式修改页的 AsyncTimeout 属性逐个请求地更改超时但是您无法将不同超时分配给从同一请求初始化的不同调用
包装它
现在您已经了解了 ASPNET 中异步页的实质它们在即将推出的 ASPNET 版本中非常易于实现并且其体系结构允许您在一个请求中批处理多个异步 I/O 操作并延迟该页的呈现直到所有操作完成通过与异步 ADONET 和 NET Framework 中的其他新异步功能相结合异步 ASPNET 页针对因充满线程池而限制可伸缩性的 I/O 绑定请求问题提供了解决方案
当生成异步页时最后需要注意的一点是不应该启动来自 ASPNET 使用的同一线程池的异步操作例如在页的异步点调用 ThreadPoolQueueUserWorkItem 会起反作用因为该方法来自线程池从而导致纯粹获取用于处理请求的零线程相反调用内置于 Framework 中的异步方法(例如方法 HttpWebRequestBeginGetResponse 和 SqlCommandBeginExecuteReader)通常认为是安全的因为这些方法倾向于使用完成端口实现异步行为