ASP.NET 异步页的实现方式
从上面的异步HttpHandler可以看到,一个处理流程被分成二个阶段了。但Page也是一个HttpHandler,不过,Page在处理请求时,有着更复杂的过程,通常被人们称为【页面生命周期】,一个页面生命周期对应着一个ASPX页的处理过程。对于同步页来说,整个过程从头到尾连续执行一遍就行了,这比较容易理解。但是对于异步页来说,它必须要拆分成二个阶段,以下图片反映了异步页的页面生命周期。注意右边的流程是代表异步页的。

这个图片是我从网上找的。原图比较小,字体较模糊,我将原图放大后又做了一番处理。本想在图片中再加点说明,考虑到尊重原图作者,没有在图片上加上任何多余字符。下面我还是用文字来补充说明一下吧。
在上面的左侧部分是一个同步页的处理过程,右侧为一个异步页的处理过程。
这里尤其要注意的是那二个红色块的步骤:它们虽然只有一个Begin与End的操作,但它们反映的是:在一个异步页的【页面生命周期】中,所有异步任务在执行时所处的阶段。 与HttpHandler不同,一个异步页可以发起多个异步调用任务。或许用所有这个词也不太恰当,您就先理解为所有吧,后面会有详细的解释。
引入这个图片只是为了能让您对于异步页的执行过程有个大致的印象:它将原来一个线程连续执行的过程分成以PreRender和PreRenderComplete为边界的二段过程,且可能会由二个不同的线程来分别处理它们。请记住这个边界,下面在演示范例时我会再次提到它们。
异步页这个词我已说过多次了,什么样的页面是一个异步页呢?
简单说来,异步页并不要求您要实现什么接口,只要在ASPX页的Page指令中,加一个【Async="true"】的选项就可以了,请参考如下代码:
很简单吧,再来看一下CodeFile中页面类的定义:
没有任何特殊的,就是一个普通的页面类。是的,但它已经是一个异步页了。有了这个基础,我们就可以为它添加异步功能了。
由于ASP.NET的异步页有 3 种实现方式,我也将分别介绍它们。请继续往下阅读。
调用Page.AddOnPreRenderCompleteAsync()的异步页
在.net的世界里,许多支持异步的原始API都采用了Begin/End的设计方式,都是基于IAsyncResult接口的。为了能方便地使用这些API,ASP.NET为它们设计了正好匹配的调用方式,那就是直接调用Page.AddOnPreRenderCompleteAsync()方法。这个方法的名字也大概说明它的功能:添加一个异步操作到PreRenderComplete事件前。我们还是来看一下这个方法的签名吧:
// 为异步页注册开始和结束事件处理程序委托。
//
// 参数:
// state:
// 一个包含事件处理程序的状态信息的对象。
//
// endHandler:
// System.Web.EndEventHandler 方法的委托。
//
// beginHandler:
// System.Web.BeginEventHandler 方法的委托。
//
// 异常:
// System.InvalidOperationException:
// <async> 页指令没有设置为 true。- 或 -System.Web.UI.Page.AddOnPreRenderCompleteAsync(System.Web.BeginEventHandler,System.Web.EndEventHandler)
// 方法在 System.Web.UI.Control.PreRender 事件之后调用。
//
// System.ArgumentNullException:
// System.Web.UI.PageAsyncTask.BeginHandler 或 System.Web.UI.PageAsyncTask.EndHandler
// 为空引用(Visual Basic 中为 Nothing)。
public void AddOnPreRenderCompleteAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
其中BeginEventHandler与EndEventHandler的定义如下:
// 表示处理异步事件(如应用程序事件)的方法。此委托在异步操作开始时调用。
//
// 返回结果:
// System.IAsyncResult,它表示 System.Web.BeginEventHandler 操作的结果。
public delegate IAsyncResult BeginEventHandler(object sender, EventArgs e, AsyncCallback cb, object extraData);
// 摘要:
// 表示处理异步事件(如应用程序事件)的方法。
public delegate void EndEventHandler(IAsyncResult ar);
如果单看以上接口的定义,可以发现除了“object sender, EventArgs e”是多余部分之外,其余部分则刚好与Begin/End的设计方式完全吻合,没有一点多余。
我们来看一下如何调用这个方法来实现异步的操作:(注意代码中的注释)
{
Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
// 准备回调数据,它将由AddOnPreRenderCompleteAsync的第三个参数被传入。
MyHttpClient<string, string> http = new MyHttpClient<string, string>();
http.UserData = textbox1.Text;
// 注册一个异步任务。注意这三个参数哦。
AddOnPreRenderCompleteAsync(BeginCall, EndCall, http);
}
private IAsyncResult BeginCall(object sender, EventArgs e, AsyncCallback cb, object extraData)
{
// 在这个方法中,
// sender 就是 this
// e 就是 EventArgs.Empty
// cb 就是 EndCall
// extraData 就是调用AddOnPreRenderCompleteAsync的第三个参数
Trace.Write("BeginCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
MyHttpClient<string, string> http = (MyHttpClient<string, string>)extraData;
// 开始一个异步调用。页面线程也最终在执行这个调用后返回线程池了。
// 中间则是等待网络的I/O的完成通知。
// 如果网络调用完成,则会调用 cb 对应的回调委托,其实就是下面的方法
return http.BeginSendHttpRequest(ServiceUrl, (string)http.UserData, cb, http);
}
private void EndCall(IAsyncResult ar)
{
// 到这个方法中,表示一个任务执行完毕。
// 参数 ar 就是BeginCall的返回值。
Trace.Write("EndCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
MyHttpClient<string, string> http = (MyHttpClient<string, string>)ar.AsyncState;
string str = (string)http.UserData;
try{
// 结束异步调用,获取调用结果。如果有异常,也会在这里抛出。
string result = http.EndSendHttpRequest(ar);
labMessage.Text = string.Format("{0} => {1}", str, result);
}
catch(Exception ex){
labMessage.Text = string.Format("{0} => Error: {1}", str, ex.Message);
}
}
对照一下异步HttpHandler中的介绍,你会发现它们非常像。
如果要执行多个异步任务,可以参考下面的代码:
{
Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
MyHttpClient<string, string> http = new MyHttpClient<string, string>();
http.UserData = textbox1.Text;
AddOnPreRenderCompleteAsync(BeginCall, EndCall, http);
MyHttpClient<string, string> http2 = new MyHttpClient<string, string>();
http2.UserData = "T2_" + Guid.NewGuid().ToString();
AddOnPreRenderCompleteAsync(BeginCall2, EndCall2, http2);
}
也很简单,就是调用二次AddOnPreRenderCompleteAsync而已。
前面我说过,异步的处理是发生在PreRender和PreRenderComplete之间,我们来还是看一下到底是不是这样的。在ASP.NET的Page中,我们很容易的输出一些调试信息,且它们会显示在所处的页面生命周期的相应执行阶段中。这个方法很简单,在Page指令中加上【Trace="true"】选项,并在页面类的代码中调用Trace.Write()或者Trace.Warn()就可以了。下面来看一下我加上调试信息的页面执行过程吧。

从这张图片中,我们至少可以看到二个信息:
所有的异步任务的执行过程确实发生在PreRender和PreRenderComplete之间。
所有的异步任务被串行地执行了。