ASP.NET 异步页的实现方式
从上面的异步HttpHandler可以看到,一个处理流程被分成二个阶段了。但Page也是一个HttpHandler,不过,Page在处理请求时,有着更复杂的过程,通常被人们称为【页面生命周期】,一个页面生命周期对应着一个ASPX页的处理过程。对于同步页来说,整个过程从头到尾连续执行一遍就行了,这比较容易理解。但是对于异步页来说,它必须要拆分成二个阶段,以下图片反映了异步页的页面生命周期。注意右边的流程是代表异步页的。
上图是网上我找的,考虑到尊重原图作者!
上面的左侧部分是一个同步页的处理过程,右侧为一个异步页的处理过程。
这里尤其要注意的是那二个红色块的步骤:它们虽然只有一个Begin与End的操作,但它们反映的是:在一个异步页的【页面生命周期】中,所有异步任务在执行时所处的阶段。 与HttpHandler不同,一个异步页可以发起多个异步调用任务。或许用所有这个词也不太恰当,您就先理解为所有吧,后面会有详细的解释。
引入这个图片只是为了能让您对于异步页的执行过程有个大致的印象:它将原来一个线程连续执行的过程分成以PreRender和PreRenderComplete为边界的二段过程,且可能会由二个不同的线程来分别处理它们。请记住这个边界,下面在演示范例时我会再次提到它们。
异步页这个词我已说过多次了,什么样的页面是一个异步页呢?
简单说来,异步页并不要求您要实现什么接口,只要在ASPX页的Page指令中,加一个【Async="true"】的选项就可以了,请参考如下代码:
- <%@ Page Language="C#" Async="true" AutoEventWireup="true" CodeFile="AsyncPage1.aspx.cs" Inherits="AsyncPage1" %>
很简单吧,再来看一下CodeFile中页面类的定义:
- public partial class AsyncPage1 : System.Web.UI.Page
没有任何特殊的,就是一个普通的页面类。是的,但它已经是一个异步页了。有了这个基础,我们就可以为它添加异步功能了。
由于ASP.NET的异步页有 3 种实现方式:
1. 调用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的设计方式完全吻合,没有一点多余。
我们来看一下如何调用这个方法来实现异步的操作:
protected void button1_click(object sender, EventArgs e)
- {
- 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中的介绍,你会发现它们非常像。
如果要执行多个异步任务,可以参考下面的代码:
- protected void button1_click(object sender, EventArgs e)
- {
- 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而已。
2. 调用Page.RegisterAsyncTask()的异步页
我一直认为ASP.NET程序也是一种服务程序,它要对客户端浏览器发出的请求而服务。由于是服务,对于要服务的对象来说,都希望能尽快地得到响应,这其实也是对服务的一个基本的要求,那就是:高吞量地快速响应。
对于前面所说的方法,显然,它的所有异步任务都是串行执行的,对于客户端来说,等待的时间会较长。而且,最严重的是,如果服务超时,上面的方法会一直等待,直到本次请求超时。为了解决这二个问题,ASP.NET定义了一种异步任务类型:PageAsyncTask 。它可以解决以上二种问题。首先我们还是来看一下PageAsyncTask类的定义:(说明:这个类的关键就是它的构造函数)
- // 使用并行执行的指定值初始化 System.Web.UI.PageAsyncTask 类的新实例。
- // state: 表示任务状态的对象。
- // executeInParallel:指示任务能否与其他任务并行处理的值。
- // endHandler:当任务在超时期内成功完成时要调用的处理程序。
- // timeoutHandler:当任务未在超时期内成功完成时要调用的处理程序。
- // beginHandler:当异步任务开始时要调用的处理程序。
- public PageAsyncTask(BeginEventHandler beginHandler, EndEventHandler endHandler,
- EndEventHandler timeoutHandler, object state, bool executeInParallel);
注意这个构造函数的签名,它与AddOnPreRenderCompleteAsync()相比,多了二个参数:EndEventHandler timeoutHandler, bool executeInParallel 。它们的含义上面的注释中有说明,这里只是提示您要注意它们而已。
创建好一个PageAsyncTask对象后,只要调用页面的RegisterAsyncTask()方法就可以注册一个异步任务。具体用法可参考我的如下代码:
- protected void button1_click(object sender, EventArgs e)
- {
- Trace.Write("ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
- // 准备回调数据,它将由PageAsyncTask构造函数的第四个参数被传入。
- MyHttpClient<string, string> httpdemo = new MyHttpClient<string, string>();
- httpdemo.UserData = textbox1.Text;
- // 创建异步任务
- PageAsyncTask task = new PageAsyncTask(BeginCall, EndCall, TimeoutCall, httpdemo);
- // 注册异步任务
- RegisterAsyncTask(task);
- }
- private IAsyncResult BeginCall(object sender, EventArgs e, AsyncCallback cb, object extraData)
- {
- // 在这个方法中,
- // sender 就是 this
- // e 就是 EventArgs.Empty
- // cb 是ASP.NET定义的一个委托,我们只管在异步调用它时把它用作回调委托就行了。
- // extraData 就是PageAsyncTask构造函数的第四个参数
- Trace.Warn("ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
- MyHttpClient<string, string> http = (MyHttpClient<string, string>)extraData;
- // 开始一个异步调用。
- return http.BeginSendHttpRequest(ServiceUrl, (string)http.UserData, cb, http);
- }
- private void EndCall(IAsyncResult ar)
- {
- // 到这个方法中,表示一个任务执行完毕。
- // 参数 ar 就是BeginCall的返回值。
- Trace.Warn("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);
- }
- }
- private void TimeoutCall(IAsyncResult ar)
- {
- // 到这个方法,就表示任务执行超时了。
- Trace.Warn("TimeoutCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
- MyHttpClient<string, string> http = (MyHttpClient<string, string>)ar.AsyncState;
- string str = (string)http.UserData;
- labMessage.Text = string.Format("{0} => Timeout.", str);
- }
前面我说过PageAsyncTask是支持超时的,那么它的超时功能是如何使用的呢,上面的示例只是给了一个超时的回调委托而已。
在开始演示PageAsyncTask的高级功能前,有必要说明一下示例所调用的服务端代码。本示例所调用的服务是【C#客户端的异步操作】中使用的演示服务,服务代码如下:
- [MyServiceMethod]
- public static string ExtractNumber(string str)
- {
- // 延迟3秒,模拟一个长时间的调用操作,便于客户演示异步的效果。
- System.Threading.Thread.Sleep(3000);
- if( string.IsNullOrEmpty(str) )
- return "str IsNullOrEmpty.";
- return new string((from c in str where Char.IsDigit(c) orderby c select c).ToArray());
- }
下面的示例我将演示开始二个异步任务,并设置异步页的超时时间为4秒钟。
- protected void button1_click(object sender, EventArgs e)
- {
- Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
- // 设置页面超时时间为4秒
- Page.AsyncTimeout = new TimeSpan(0, 0, 4);
- // 注册第一个异步任务
- MyHttpClient<string, string> http = new MyHttpClient<string, string>();
- http.UserData = textbox1.Text;
- PageAsyncTask task = new PageAsyncTask(BeginCall, EndCall, TimeoutCall, http);
- RegisterAsyncTask(task);
- // 注册第二个异步任务
- MyHttpClient<string, string> http2 = new MyHttpClient<string, string>();
- http2.UserData = "T2_" + Guid.NewGuid().ToString();
- PageAsyncTask task2 = new PageAsyncTask(BeginCall2, EndCall2, TimeoutCall2, http2);
- RegisterAsyncTask(task2);
- }
此页面的执行过程如下:
确实,第二个任务执行超时了。
再来看一下PageAsyncTask所支持的任务的并行执行是如何调用的:
- protected void button1_click(object sender, EventArgs e)
- {
- Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
- // 设置页面超时时间为4秒
- Page.AsyncTimeout = new TimeSpan(0, 0, 4);
- // 注册第一个异步任务
- MyHttpClient<string, string> http = new MyHttpClient<string, string>();
- http.UserData = textbox1.Text;
- PageAsyncTask task = new PageAsyncTask(BeginCall, EndCall, TimeoutCall, http, true /*注意这个参数*/);
- RegisterAsyncTask(task);
- // 注册第二个异步任务
- MyHttpClient<string, string> http2 = new MyHttpClient<string, string>();
- http2.UserData = "T2_" + Guid.NewGuid().ToString();
- PageAsyncTask task2 = new PageAsyncTask(BeginCall2, EndCall2, TimeoutCall2, http2, true /*注意这个参数*/);
- RegisterAsyncTask(task2);
- }
此页面的执行过程如下:
图片清楚地反映出,这二个任务是并行执行时,所以,这二个任务能在4秒内同时执行完毕。
在结束对PageAsyncTask的介绍前,有必要对超时做个说明。对于使用PageAsyncTask的异步页来说,有二种方法来设置超时时间:
1. 通过Page指令: asyncTimeout="0:00:45" ,这个值就是异步页的默认值。至于这个值的含义,我想您应该懂的。
2. 通过设置 Page.AsyncTimeout = new TimeSpan(0, 0, 4); 这种方式。示例代码就是这种方式。
注意:由于AsyncTimeout是Page级别的参数,因此,它是针对所有的PageAsyncTask来限定的,并非每个PageAsyncTask的超时都是这个值。
3. 基于事件模式的异步页
ASP.NET的异步页中的实现代码如下:
- private void CallViaEvent(string str)
- {
- MyAysncClient<string, string> client = new MyAysncClient<string, string>(ServiceUrl);
- client.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client_OnCallCompleted);
- client.CallAysnc(str, str);
- }
- void client_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e)
- {
- Trace.Warn("client_OnCallCompleted ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
- if( e.Error == null )
- labMessage.Text = string.Format("{0} => {1}", e.UserState, e.Result);
- else
- labMessage.Text = string.Format("{0} => Error: {1}", e.UserState, e.Error.Message);
- }
再来看一下如何发出多个异步任务:
- protected void button1_click(object sender, EventArgs e)
- {
- Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());
- string str = textbox1.Text;
- // 注意:这个异步任务,我设置了2秒的超时。它应该是不能按时完成任务的。
- MyAysncClient<string, string> client = new MyAysncClient<string, string>(ServiceUrl, 2000);
- client.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client_OnCallCompleted);
- client.CallAysnc(str, str); // 开始第一个异步任务
- string str2 = "T2_" + Guid.NewGuid().ToString();
- MyAysncClient<string, string> client2 = new MyAysncClient<string, string>(ServiceUrl);
- client2.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client2_OnCallCompleted);
- client2.CallAysnc(str2, str2); // 开始第二个异步任务
- }
- void client2_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e)
- {
- ShowCallResult(2, e);
- // 再来一个异步调用
- string str3 = "T3_" + Guid.NewGuid().ToString();
- MyAysncClient<string, string> client3 = new MyAysncClient<string, string>(ServiceUrl);
- client3.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client3_OnCallCompleted);
- client3.CallAysnc(str3, str3); // 开始第三个异步任务
- }
页面的执行过程如下图:
【基于事件模式的异步】的开始阶段并不一定要PreRender事件之后,而对于前二种异步面的实现方式则是肯定在PreRender事件之后。