片清楚地反映出,
这二个任务是并行执行时,所以,这二个任务能在4秒内同时执行完毕。
在结束对PageAsyncTask的介绍前,有必要对超时做个说明。 对于使用PageAsyncTask的异步页来说,有二种方法来设置超时时间:
1. 通过Page指令: asyncTimeout="0:00:45" ,这个值就是异步页的默认值。至于这个值的含义,我想您应该懂的。
2. 通过设置 Page.AsyncTimeout = new TimeSpan(0, 0, 4); 这种方式。示例代码就是这种方式。
注意:由于AsyncTimeout是Page级别的参数,因此,它是针对所有的PageAsyncTask来限定的,并非每个PageAsyncTask的超时都是这个值。
3. 基于事件模式的异步页
如果您看过我的博客【C#客户端的异步操作】, 那么对【基于事件模式的异步】这个词就不会再感到陌生了。在那篇博客中,我就对这种异步模式做过介绍, 只不是,上次是在WinForm程序中演示的而已。为了方便对比,我再次把那段代码贴出来:
/// <summary>
/// 基于事件的异步模式
/// </summary>
/// <param name="str"></param>
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)
{
//bool flag = txtOutput.InvokeRequired; // 注意:这里flag的值是false,也就是说可以直接操作UI界面
if( e.Error == null )
ShowResult(string.Format("{0} => {1}", e.UserState, e.Result));
else
ShowResult(string.Format("{0} => Error: {1}", e.UserState, e.Error.Message));
}
上次,我就解释过,这种方法在WinForm中非常方便。幸运的是,ASP.NET的异步页也支持这种方式。
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); // 开始第三个异步任务
}
页面的执行过程如下图:
这里要说明一下了:在【C#客户端的异步操作】中我就给出这个类的实现代码, 不过,这次我给它增加了超时功能,增加了一个重载的构造函数,需要在构造函数的第二个参数传入。 今天我就不贴出那个类的代码了,有兴趣的自己去下载代码阅读吧。 在上次贴的代码,你应该可以发现,在CallAysnc()时,就已经开始了异步操作。对于本示例来说,也就是在button1_click就已经开始了二个异步操作。
这是个什么意思呢?
可以这样来理解:前二个任务显然是和LoadComplete,PreRender事件阶段的代码在并行执行的。
有意思的是:第三个任务是在第二个任务的结束事件中开始的,但三个任务的结束操作全在页面的PreRender事件才得到处理。 下面我再把这个例子来改一下,就更有趣了:
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); // 开始第一个异步任务
System.Threading.Thread.Sleep(3000);
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); // 开始第二个异步任务
}
现在,在第一个任务发出后,我让线程等待了3秒,也就是等到了第一个任务的超时。然后再开始第二个任务。
也就是说:在button1_click事件还没执行完毕,第一个任务就结束了。
现在,您可以猜一下,此时的执行过程是个什么样的。
猜好了就来看下图吧。
现在明白了吧:哪怕是在PostBackEvent阶段就结束的任务,也要等到PreRender之后才能得到处理。
至于为什么会是这样的,我以后再讲。今天只要记住本文的第一张图片就好了。
我可是好不容易才找出这张图片来的,且为了让您能看得更清楚,还花了些时间修改了它。
在那个图片后面我还说过:在一个异步页的【页面生命周期】中,所有异步任务在执行时所处的阶段。并在后面注明了这里的所有这个词也不太恰当。现在可以解释为什么不恰当了:
【基于事件模式的异步】的开始阶段并不一定要PreRender事件之后,而对于前二种异步面的实现方式则是肯定在PreRender事件之后。
至于这其中的原因,同样,您要等待我的后续博客了。
各种异步页的实现方式比较
前面介绍了3种异步页的实现方式,我打算在这里给它们做个总结及比较。当然,这一切只代表我个人的观点,仅供参考。
为了能给出一个客观的评价,我认为先有必要再给个示例,把这些异步方式放在一起执行,就好像把它们放在一起比赛一样, 或许这样会更有意思,同时也会让我给出的评价更有说服力。
在下面的示例中,我把上面说过的3种异步方式放在一起,并让每种方法执行多次(共10个异步任务),实验代码如下:
protected void button1_click(object sender, EventArgs e)
{
ShowThreadInfo("button1_click");
// 为PageAsyncTask设置超时时间
Page.AsyncTimeout = new TimeSpan(0, 0, 7);
// 开启4个PageAsyncTask,其中第1,4个任务不接受并行执行,2,3则允许并行执行
Async_RegisterAsyncTask("RegisterAsyncTask_1", false);
Async_RegisterAsyncTask("RegisterAsyncTask_2", true);
Async_RegisterAsyncTask("RegisterAsyncTask_3", true);
Async_RegisterAsyncTask("RegisterAsyncTask_4", false);
// 开启3个AddOnPreRenderCompleteAsync的任务
Async_AddOnPreRenderCompleteAsync("AddOnPreRenderCompleteAsync_1");
Async_AddOnPreRenderCompleteAsync("AddOnPreRenderCompleteAsync_2");
Async_AddOnPreRenderCompleteAsync("AddOnPreRenderCompleteAsync_3");
// 最后开启3个基于事件通知的异步任务,其中第2个任务由于设置了超时,将不能成功完成。
Async_Event("MyAysncClient_1", 0);
Async_Event("MyAysncClient_2", 2000);
Async_Event("MyAysncClient_3", 0);
}
private void Async_RegisterAsyncTask(string taskName, bool executeInParallel)
{
MyHttpClient<string, string> http = new MyHttpClient<string, string>();
http.UserData = taskName;
PageAsyncTask task = new PageAsyncTask(BeginCall_Task, EndCall_Task, TimeoutCall_Task, http, executeInParallel);
RegisterAsyncTask(task);
}
private void Async_AddOnPreRenderCompleteAsync(string taskName)
{
MyHttpClient<string, string> http = new MyHttpClient<string, string>();
http.UserData = taskName;
AddOnPreRenderCompleteAsync(BeginCall, EndCall, http);
}
private void Async_Event(string taskName, int timeoutMilliseconds)
{
MyAysncClient<string, string> client = new MyAysncClient<string, string>(ServiceUrl, timeoutMilliseconds);
client.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client_OnCallCompleted);
client.CallAysnc(taskName, taskName);
}
执行过程如下图:
不知您看到这个执行过程是否会想到为什么会是这个样子的。至于为什么会是这个样子的, 这就涉及到ASP.NET的异步页的执行过程,这个过程比较复杂,我以后再谈。 今天咱们就来根据这个图片来谈谈比较表面化的东西,谈一下这三种方式的差别。
从上面的代码以及执行过程,可以看到一个有趣的现象,我明明是先注册的4个PageAsyncTask 。 可是呢,最先显示的却是【BeginCall AddOnPreRenderCompleteAsync_1】。 我想我这里使用显示这个词也是比较恰当的,为什么呢?因为,我前面已经解释过了, 基于事件的异步的任务应该是在button1_click事件处理器中先执行的,只是我没有让它们显示罢了。 接下来的故事也很自然,由于我将"MyAysncClient_2"设置为2秒的超时,它最先完成,只是结果为超时罢了。 紧接着,"MyAysncClient_1"和"MyAysncClient_3"也执行结束了。嗯,是的:3个事件的异步任务全执行完了。
说到这里我要另起一段了,以提醒您的注意。
有没有注意到,前面说到的3个事件的异步任务全执行完了。这个时候,其它的异步任务绝大部分还没有开始呢, 它们3个咋就先执行完了呢?
有意思吧,其实何止3个,如果再来5个基于事件的异步任务,它们还是会先执行完成,不信的话,看下图:
或许举这个例子把基于事件的异步方式捧高了。这里我也要客观的解释一下原因了:
出现这个现象主要由2个原因造成的:
1. 在这个例子中,"MyAysncClient_1", "MyAysncClient_2", "MyAysncClient_3", "AddOnPreRenderCompleteAsync_1"由于都是异步任务,所以基本上是并行执行的,
2. 由于3个基于事件的异步方式先执行的,因此它们先结束了。
接着来解释图片所反映的现象。当基于事件的异步任务全执行完成后," EndCall AddOnPreRenderCompleteAsync_1"也被调用了。说明"AddOnPreRenderCompleteAsync_1"这个任务彻底地执行完了。 接下来,"AddOnPreRenderCompleteAsync_2","AddOnPreRenderCompleteAsync_3"也依次执行完了。
我一开始用RegisterAsyncTask注册的4个异步任务呢?终于,在前面的所有异步任务全部执行完成后, 才开始了这类任务的执行过程。首先执行的是"RegisterAsyncTask_1",这个好理解。 接下来,"BeginCall RegisterAsyncTask_2", "BeginCall RegisterAsyncTask_3"被连续调用了, 这也好理解吧,因为我当时创建异步任务时,指定它们是允许与其它任务并行执行的,因此它们是一起执行的。3秒后,2个任务同时执行完了,最后启动了"RegisterAsyncTask_4",由于它不支持并行执行,所以,它排在最后, 在没有任何悬念中,"TimeoutCall RegisterAsyncTask_4"被调用了。这么正常啊,我设置过Page.AsyncTimeout = new TimeSpan(0, 0, 7);因此,前二批PageAsyncTask赶在超时前正常结束了,留给"RegisterAsyncTask_4"的执行时间只有1秒,它当然就不能在指定时间内正常完成。
似乎到这里,这些异步任务的执行过程都解释完了,但是,有二个很奇怪的现象您有没有发现:
1. 为什么AddOnPreRenderCompleteAsync的任务全执行完了之后,才轮到PageAsyncTask的任务呢?
2. 还有前面说过的,为什么是"BeginCall AddOnPreRenderCompleteAsync_1"最先显示呢?
这一切绝非偶然,如果您有兴趣,可下载我的示例代码,你运行千遍万遍还将是这个结果。
这些原因我以后再谈,今天的博客只是想告诉您这样一个结果就行了。
不过,为了能让您能容易地理解后面的内容,我暂且告诉您:PageAsyncTask是建立在AddOnPreRenderCompleteAsync的基础上的。
有了前面这些实验结果,我们再来对这3种异步页方法做个总结及比较。
1. AddOnPreRenderCompleteAsync: 它提供了最基本的异步页的使用方法。就好像HttpHandler一样,它虽能处理请求,但不太方便,显得比较原始。 由于它提供的是比较原始的方法,您也可以自行包装您的高级功能。
2. PageAsyncTask: 与AddOnPreRenderCompleteAsync相比,它增加了超时以及并行执行的功能,但我也说过,它是建立在AddOnPreRenderCompleteAsync的基础之上的。 如果把AddOnPreRenderCompleteAsync比作为HttpHandler,那么PageAsyncTask则就像是Page 。因此它只是做了些高级的包装罢了。
3. 基于事件的异步方式:与前2者完全没有关系,它只依赖于AspNetSynchronizationContext。这里有必要强调一下: 【基于事件的异步方式】可以理解为一个设计模式,也可以把它理解成对最基础的异步方式的高级包装。 它能提供或者完成的功能,依赖于包装的方式及力度。 在我提供的这个包装类中,它也可以实现与PageAsyncTask一样的并行执行以及超时功能。
后二种方法功能强大的原因是来源于高级包装,由于包装,过程也会更复杂,因此性能或许也会有微小的损失。 如果您不能接受这点性能损失,可能还是选AddOnPreRenderCompleteAsync会比较合适。 不过,我要再次提醒您:它不支持并行执行,不支持超时。
请容忍我再夸一下【基于事件的异步模式】,从我前面的示例代码,尤其是与WinForm中的示例代码的比较中, 我们可以清楚的发现,这种方式是非常易用的。掌握了这种方式,至少在这二大编程模型中都是适用的。 而且,它能在异步页的执行周期中,较早的进入异步等待状态,因此能更快的结束执行过程。 想想【从"Begin Raise PostBackEvent"到"End PreRender"这中间还可以执行多少代码是不确定的】吧。
【基于事件的异步模式】的优点不仅如此,我的演示代码中还演示了另一种用法:在一个完成事件中,我还能再开启另一个异步任务。这个优点使我可以有选择性地启动后续的异步操作。但是,这个特性是另2个不可能做到的! 这个原因可以简单地表达为:在PreRender事件后,调用AddOnPreRenderCompleteAsync会抛异常。
异步HttpModule的实现方式
在【用Asp.net写自己的服务框架】中, 我示范过如果编写一个HttpModule,通常只要我们实现IHttpModule接口,并在Init方法中订阅一些事件就可以了:
internal class DirectProcessRequestMoudle : IHttpModule
{
public void Init(HttpApplication app)
{
app.PostAuthorizeRequest += new EventHandler(app_PostAuthorizeRequest);
}
HttpHandler有异步接口的IHttpAsyncHandler,但HttpModule却只有一个接口:IHttpModule,不管是同步还是异步。 异步HttpModule的实现方式并不是订阅HttpApplication的事件,而是调用HttpApplication的一些注册异步操作的方法来实现的(还是在Init事件中), 这些方法可参考以下列表:
// 将指定的 System.Web.HttpApplication.AcquireRequestState 事件
// 添加到当前请求的异步 System.Web.HttpApplication.AcquireRequestState事件处理程序的集合。
public void AddOnAcquireRequestStateAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.AuthenticateRequest 事件
// 添加到当前请求的异步 System.Web.HttpApplication.AuthenticateRequest事件处理程序的集合。
public void AddOnAuthenticateRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.AuthorizeRequest 事件
// 添加到当前请求的异步 System.Web.HttpApplication.AuthorizeRequest事件处理程序的集合。
public void AddOnAuthorizeRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.BeginRequest 事件
// 添加到当前请求的异步 System.Web.HttpApplication.BeginRequest事件处理程序的集合。
public void AddOnBeginRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.EndRequest 事件
// 添加到当前请求的异步 System.Web.HttpApplication.EndRequest事件处理程序的集合。
public void AddOnEndRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
public void AddOnLogRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
public void AddOnMapRequestHandlerAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.PostAcquireRequestState 事件
// 添加到当前请求的异步 System.Web.HttpApplication.PostAcquireRequestState事件处理程序的集合。
public void AddOnPostAcquireRequestStateAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.PostAuthenticateRequest 事件
// 添加到当前请求的异步 System.Web.HttpApplication.PostAuthenticateRequest事件处理程序的集合。
public void AddOnPostAuthenticateRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.PostAuthorizeRequest 事件
// 添加到当前请求的异步 System.Web.HttpApplication.PostAuthorizeRequest事件处理程序的集合。
public void AddOnPostAuthorizeRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
public void AddOnPostLogRequestAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.PostMapRequestHandler 事件
// 添加到当前请求的异步 System.Web.HttpApplication.PostMapRequestHandler事件处理程序的集合。
public void AddOnPostMapRequestHandlerAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.PostReleaseRequestState 事件
// 添加到当前请求的异步 System.Web.HttpApplication.PostReleaseRequestState事件处理程序的集合。
public void AddOnPostReleaseRequestStateAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.PostRequestHandlerExecute 事件
// 添加到当前请求的异步 System.Web.HttpApplication.PostRequestHandlerExecute事件处理程序的集合。
public void AddOnPostRequestHandlerExecuteAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.PostResolveRequestCache 事件
// 添加到当前请求的异步 System.Web.HttpApplication.PostResolveRequestCache事件处理程序的集合。
public void AddOnPostResolveRequestCacheAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.PostUpdateRequestCache 事件
// 添加到当前请求的异步 System.Web.HttpApplication.PostUpdateRequestCache事件处理程序的集合。
public void AddOnPostUpdateRequestCacheAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.PreRequestHandlerExecute 事件
// 添加到当前请求的异步 System.Web.HttpApplication.PreRequestHandlerExecute事件处理程序的集合。
public void AddOnPreRequestHandlerExecuteAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.ReleaseRequestState 事件
// 添加到当前请求的异步 System.Web.HttpApplication.ReleaseRequestState事件处理程序的集合。
public void AddOnReleaseRequestStateAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.ResolveRequestCache 事件处理程序
// 添加到当前请求的异步 System.Web.HttpApplication.ResolveRequestCache事件处理程序的集合。
public void AddOnResolveRequestCacheAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
// 将指定的 System.Web.HttpApplication.UpdateRequestCache 事件
// 添加到当前请求的异步 System.Web.HttpApplication.UpdateRequestCache事件处理程序的集合。
public void AddOnUpdateRequestCacheAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state);
每个方法的含义从它们的名字是可以看出。 异步HttpModule的实现方式需要将异步对应的Begin/End二个方法分别做为委托参数传入这些方法中。
注意:这些方法的签名与Page.AddOnPreRenderCompleteAsync()是一致的,因此它们的具体用法也与Page.AddOnPreRenderCompleteAsync()一样。
为什么这里不设计成订阅事件的方式?
我想是因为:如果采用事件模式,调用者可以只订阅其中的一个事件,ASP.NET不容易控制,还有"object state"这个参数不便于在订阅事件时传入。
异步HttpModule的示例代码如下:
/// <summary>
/// 【示例代码】演示异步的HttpModule
/// 说明:这个示例一丁点意义也没有,纯粹是为了演示。
/// </summary>
public class MyAsyncHttpModule : IHttpModule
{
public static readonly object HttpContextItemsKey = new object();
private static readonly string s_QueryDatabaseListScript =
@"select dtb.name from master.sys.databases as dtb order by 1";
private static readonly string s_ConnectionString =
@"server=localhost\sqlexpress;Integrated Security=SSPI;Asynchronous Processing=true";
public void Init(HttpApplication app)
{
// 注册异步事件
app.AddOnBeginRequestAsync(BeginCall, EndExecuteReader, null);
}
private IAsyncResult BeginCall(object sender, EventArgs e, AsyncCallback cb, object extraData)
{
SqlConnection connection = new SqlConnection(s_ConnectionString);
connection.Open();
SqlCommand command = new SqlCommand(s_QueryDatabaseListScript, connection);
CallbackParam cbParam = new CallbackParam {
Command = command,
Context = HttpContext.Current
};
return command.BeginExecuteReader(cb, cbParam);
}
private class CallbackParam
{
public SqlCommand Command;
public HttpContext Context;
}
private void EndExecuteReader(IAsyncResult ar)
{
CallbackParam cbParam = (CallbackParam)ar.AsyncState;
StringBuilder sb = new StringBuilder();
try {
using( SqlDataReader reader = cbParam.Command.EndExecuteReader(ar) ) {
while( reader.Read() ) {
sb.Append(reader.GetString(0)).Append("; ");
}
}
}
catch( Exception ex ) {
cbParam.Context.Items[HttpContextItemsKey] = ex.Message;
}
finally {
cbParam.Command.Connection.Close();
}
if( sb.Length > 0 )
cbParam.Context.Items[HttpContextItemsKey] = "数据库列表:" + sb.ToString(0, sb.Length - 2);
}
public void Dispose()
{
}
}
页面可以使用如下方式获得MyAsyncHttpModule的结果:
public partial class TestMyAsyncHttpModule : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string result = (string)HttpContext.Current.Items[MyAsyncHttpModule.HttpContextItemsKey]
?? "没有开启MyAsyncHttpModule,请在web.config中启用它。";
Response.Write(result);
}
}
说明:管线处理过程中,可能有多个HttpModule,但是异步的HttpModule在执行时,只是在一个阶段内,所有的HttpModule采用异步方式工作。 当进入下一个阶段前,必须要等到所有HttpModule全部在当前阶段内执行完毕。
通常情况下,是没有必要写异步的HttpModule的。这是我写的第一个异步HttpModule。
异步的 Web Service
由于Web Service也是受ASP.NET支持,且随着ASP.NET一起出现。我们再来看一下如果将一个同步的服务方法改变成异步的方法。
注意:将方法由同步改成异步版本,是不影响客户端的。
以下代码是一个同步版本的服务方法:
[WebMethod]
public string ExtractNumber(string str)
{
//return ........
}
再来看一下最终的异步实现版本:
[WebMethod]
public IAsyncResult BeginExtractNumber(string str, AsyncCallback cb, object state)
{
MyHttpClient<string, string> http = new MyHttpClient<string, string>();
http.UserData = "Begin ThreadId: " + Thread.CurrentThread.ManagedThreadId.ToString();
return http.BeginSendHttpRequest(ServiceUrl, str, cb, http);
}
[WebMethod]
public string EndExtractNumber(IAsyncResult ar)
{
MyHttpClient<string, string> http = (MyHttpClient<string, string>)ar.AsyncState;
try{
return http.EndSendHttpRequest(ar) +
", " + http.UserData.ToString() +
", End ThreadId: " + Thread.CurrentThread.ManagedThreadId.ToString();
}
catch(Exception ex){
return ex.ToString();
}
}
其实,要做的修改与IHttpHandler到IHttpAsyncHandler的工作差不多,在原有的同步方法后面加二个与异步操作有关的参数, 并且返回值改为IAsyncResult,然后再添加一个EndXxxx方法就可以了,当然了,EndXxxx方法的传入参数只能是一个IAsyncResult类型的参数。
ASP.NET MVC 中的异步方式
在ASP.NET MVC框架中,感觉一下回到原始社会中,简直和异步页的封装没法比。来看代码吧。(注意代码中的注释)
// 实际可处理的Action名称为 Test1 ,注意名称后要加上 Async
public void Test1Async()
{
// 告诉ASP.NET MVC,要开始一个异步操作了。
AsyncManager.OutstandingOperations.Increment();
string str = Guid.NewGuid().ToString();
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)
{
// 告诉ASP.NET MVC,一个异步操作结束了。
AsyncManager.OutstandingOperations.Decrement();
if( e.Error == null )
AsyncManager.Parameters["result"] = string.Format("{0} => {1}", e.UserState, e.Result);
else
AsyncManager.Parameters["result"] = string.Format("{0} => Error: {1}", e.UserState, e.Error.Message);
// AsyncManager.Parameters["result"] 用于写输出结果。
// 这里仍然采用类似ViewData的设计。
// 注意:key 的名称要和Test1Completed的参数名匹配。
}
// 注意名称后要加上 Completed ,且其余部分与Test1Async的前缀对应。
public ActionResult Test1Completed(string result)
{
ViewData["result"] = result;
return View();
}
说明:如果您认为单独为事件处理器写个方法看起来不爽,您也可以采用匿名委托之类的闭包写法,这个纯属个人喜好问题。
再来个多次异步操作的示例:
public void Test2Async()
{
// 表示要开启3个异步操作。
// 如果把这个数字设为2,极有可能会产生的错误的结果。不信您可以试一下。
AsyncManager.OutstandingOperations.Increment(3);
string str = Guid.NewGuid().ToString();
MyAysncClient<string, string> client = new MyAysncClient<string, string>(ServiceUrl);
client.UserData = "result1";
client.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client2_OnCallCompleted);
client.CallAysnc(str, str); // 开始第一个异步任务
string str2 = "T2_" + Guid.NewGuid().ToString();
MyAysncClient<string, string> client2 = new MyAysncClient<string, string>(ServiceUrl);
client2.UserData = "result2";
client2.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client2_OnCallCompleted);
client2.CallAysnc(str2, str2); // 开始第二个异步任务
string str3 = "T3_" + Guid.NewGuid().ToString();
MyAysncClient<string, string> client3 = new MyAysncClient<string, string>(ServiceUrl);
client3.UserData = "result3";
client3.OnCallCompleted += new MyAysncClient<string, string>.CallCompletedEventHandler(client2_OnCallCompleted);
client3.CallAysnc(str3, str3); // 开始第三个异步任务
}
void client2_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e)
{
// 递减内部的异步任务累加器。有点类似AspNetSynchronizationContext的设计。
AsyncManager.OutstandingOperations.Decrement();
MyAysncClient<string, string> client = (MyAysncClient<string, string>)sender;
string key = client.UserData.ToString();
if( e.Error == null )
AsyncManager.Parameters[key] = string.Format("{0} => {1}", e.UserState, e.Result);
else
AsyncManager.Parameters[key] = string.Format("{0} => Error: {1}", e.UserState, e.Error.Message);
}
public ActionResult Test2Completed(string result1, string result2, string result3)
{
ViewData["result1"] = result1;
ViewData["result2"] = result2;
ViewData["result3"] = result3;
return View();
}
我来解释一下上面的代码是如何以异步方式工作的。首先,我们要把Controller的基类修改为AsyncController,代码如下:
public class HomeController : AsyncController
假如我有一个同步的Action方法:Test1,它看起来应该是这样的:
public ActionResult Test1()
{
return View();
}
首先,我需要把它的返回值改成void, 并把方法名称修改为Test1Async 。
然后,在开始异步调用前,调用AsyncManager.OutstandingOperations.Increment();
在异步完成时:
1. 要调用AsyncManager.OutstandingOperations.Decrement();
2. 将结果写入到AsyncManager.Parameters[]这个集合中。注意key的名字后面要用到。
到这里,异步开发的任务算是做了一大半了。你可能会想我在哪里返回ActionResult呢?
再来创建一个Test1Completed方法,签名应该是这个样子的:
public ActionResult Test1Completed(string result)
注意:方法中的参数名要和前面说过的写AsyncManager.Parameters[]的key名一致,包括数量。
再后面的事情,我想您懂的,我就不多说了。
再来说说我对【ASP.NET MVC的异步方式】这个设计的感受吧。
简单说来就是:不够完美。
要知道在这个例子中,我可是采用的基于事件的异步模式啊,在异步页中,哪有这些额外的调用?
对于这个设计,我至少有2点不满意:
1. AsyncManager.OutstandingOperations.Increment(); Decrement();由使用者来控制,容易出错。
2. AsyncManager.Parameters[]这个bag设计方式也不爽,难道仅仅是为了简单?因为我可以在完成事件时,根据条件继续后面的异步任务,最终结果可能并不确定,因此后面的XXXXCompleted方法的签名就是个问题了。
为什么在ASP.NET MVC中,这个示例需要调用Increment(); Decrement(),而在异步页中不需要呢?
恐怕有些人会对此有好奇,我就告诉大家吧:这与AspNetSynchronizationContext有关。
AspNetSynchronizationContext,真是个【成也萧何,败成萧何】的东西,在异步页为什么不需要我们调用类似Increment(); Decrement()的语句是因为, 它内部也有个这样的累加器,不过,当时在设计基于事件的异步模式时,在ASP.NET运行环境中,SynchronizationContext就是使用了AspNetSynchronizationContext这个具体实现类, 但它的绝大部分成员却是internal类型的。如果可以使用它,可以用一种简便地方式设置一个统一的回调委托:
if( this._syncContext.PendingOperationsCount > 0 ) {
this._syncContext.SetLastCompletionWorkItem(this._callHandlersThreadpoolCallback);
}
就这么一句话,可以不用操心使用者到底开始了多少个异步任务,都可以在所有的异步结束后,回调指定的委托。只是可惜的是,这二个成员都是internal的!
如果当初微软设计AspNetSynchronizationContext时,不开放SetLastCompletionWorkItem这个方法, 是担心使用者乱调用导致ASP.NET运行错误的话,现在ASP.NET MVC的这种设计显然更容易出错。 当然了,ASP.NET MVC出来的时候,这一切早就出现了,因此它也无法享受AspNetSynchronizationContext的便利性。 不过,最让我想不通的是:直到ASP.NET 4.0,这一切还是原样。 难道是因为ASP.NET MVC独立在升级,连InternalsVisibleTo的机会也不给它吗?
就算我们不用基于事件的异步模式,异步页还有二种实现方法呢(都不需要累加器),可是ASP.NET MVC却没有实现类似的功能。 所以,这样就显得很不完善。我们也只能期待未来的版本能改进这些问题了。
MSDN参考文章:在 ASP.NET MVC 中使用异步控制器
受争论的【基于事件的异步模式】
本来在我的写作计划中,是没有这段文字的,可就在我打算发布这篇博客之前,想到上篇博客中的评论,突然我想到一本书:CLR via C# 。 是的,就是这本书,我想很多人手里有这本书,想到这本书是因为上篇博客的评论中,出现一个与我的观点有着不一致的声音(来自AndersTan),而他应该是Jeffer Richter的粉丝。 我早就买了这本书了(中文第三版),其实也是AndersTan推荐的,不过一直没有看完, 因此,根本就没有发现Jeffer Richter是【基于事件的异步模式】的反对者, 这个可参考书中696页。Jeffer Richter在书中说:“由于我不是EAP的粉丝,而且我不赞同使用这个模式,所以一直没有花太多的时间在它上面。然而,我知道有一些人确实喜欢这个模式,而且想使用它,所以我专门花了一些时间研究它。”为了表示对大牛的敬重,我用蓝色字体突出他说的话(当然是由周靖翻译的)。看到这句话以及后面他对于此模式的评价,尤其是在 【27.11.2 APM和EAP的对比】这个小节中对于EAP的评价,让我感觉大牛其实也没有很有地了解这个模式。
这里再补充一下,书中提到二个英文简写:EAP: Event-base Asynchronous Pattern, APM: Asynchronous Programming Model 。书中689页中,Jeffer Richter还说过:“虽然我是APM的超级粉丝,但是我必须承认它存在的一些问题。”与之相反,虽然我不是APM的忠实粉丝,我却不认为他所说的问题真的是APM的缺点。他说的第一点,感觉就没有意义。 我不知道有多少人在现实使用中,是在调用了Begin方法后,立即去调用End方法? 我认为.net允许这种使用方式,可能还是更看中的是使用上的灵活性,毕竟微软要面对的开发者会有千奇百怪的要求。 而且MSDN中也解释了这种调用会阻塞线程。访问IAsyncResult是可以一个WaitHandle, 这个好像在上篇博客的评论中有人也提过了,我当时也不想说了,这次就把我的实现方式贴出来了,只希望告诉一些人:这个成员虽然是个耗资源的东西, 但要看你如何去实现它了:有些时候(完成的时候)可以返回null的,所以,通常应该设计成一种延迟创建模式才对(我再一次的提醒:在设计它时要考虑多线程的并发访问)。
刚才扯远了,我们还是来说关于Jeffer Richter对于【27.11.2 APM和EAP的对比】这个小节的看法(699页)。这个小节有4个段话,分别分4个方面说了些EAP的【缺点】, 我也将依次来发表我的观点。
1. Jeffer Richter认为EAP的最大优点在于和IDE的配合使用,且在后面一直提到GUI线程。 显然EAP模式被代表了,被WinForm这类桌面程序程序代表了。 我今天的示例代码一直是可以在ASP.NET环境下运行的,而且还特意和WinForm下的使用方法做了比较,结果是:使用方式基本相同。我认为这个结果才是EAP模式最大的优点:在不同的编程模型中不必考虑线程模型问题。
2. Jeffer Richter说:事实上,EAP必须为引发的所有进度报告和完成事件分配从EventArgs派生的对象......。 看到这句话的感觉还是和上句话差不多:被代表了。 对于这段话,我认为有必要从几个角度来表达我的观点:
a. 进度报告:我想问一句:ASP.NET编程模型下进度报告有什么意义,或者说如何实现? 在我今天演示的示例代码中,我一直没演示进度报告吧?事实上,我的包装类中根本就不提供这个功能,只提供了完成事件的功能。 再说,为什么需要进度报告?因为桌面程序需要,它们为了能让程序拥有更好的用户体验。当然也可以不提供进度报告嘛, 大不了让用户守在电脑面前傻等就是了,这样还会有性能损失吗?当然没有,但是用户可能会骂人......。
b. 性能损失:MyAysncClient是对一个更底层的静态方法调用的封装。我也很明白:有封装就有性能的损失。但我想:一次异步任务也就只通知一次,性能损失能有多大? 而且明知道有性能损失,我为什么还要封装呢?只为一个很简单的理由:使用起来更容易!
c. 对象的回收问题:如果按照Jeffer Richter的说法,多创建这几个对象就让GC为难的话,会让我对.NET失去信心,连ASP.NET也不敢用了, 因为要知道.NET的世界是完全面向对象的世界,一次WEB请求的处理过程中,ASP.NET不知道要创建多少个对象,我真的数不清楚。
3. Jeffer Richter说:如果在登记事件处理方法之前调用XxxAsync方法,......。看到这里,我笑了。 显然,大牛是非常讨厌EAP模式的。EAP是使用了事件,这个错误的调用顺序问题如果是EAP的错,那么.NET的事件模式就是个错误的设计。 大牛说这句真是不负责任嘛。
4. Jeffer Richter说:“EAP的错误处理和系统的其余部分也不一致,首先,异步不会抛出。在你的事件处理方法中,必须查询;AsyncCompletedEventArgs的Exception属性,看它是不是null ......”看到这句话,我突然想到:一个月前在同事的桌上看到Jeffery Zhao 在【2010第二届.NET技术与IT管理技术大会 的一个 The Evolution of Async Programming on .NET Platform】培训PPT,代码大致是这样写的:
class XxxCompletedEventArgs : EventArgs {
Exception Error { get; }
TResult Result { get; }
}
所以,我怀疑:Jeffer Richter认为EAP模式在完成时的事件中,异常也结果也是这样分开来处理的!
大家不妨回想一下,回到Jeffery Richter所说的APM模式下,我们为了能得到异步调用的结果,去调用End方法, 结果呢,如果异步在处理时,有异常发生了,此时会抛出来。是的,我也同意使用这种方式来明确的告之调用者:此时没有结果,只有异常。
我们还是再来看一下我前面一直使用的一段代码:
void client_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e)
{
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);
}
表面上看,这段代码确实有Jeffer Richter所说的问题:有异常不会主动抛出。
这里有必要说明一下:有异常不主动抛出,而是依赖于调用者判断返回结果的设计方式,是不符合.NET设计规范的。 那我如果把代码写成下面的这样呢?
void client_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e)
{
try {
labMessage.Text = string.Format("{0} => {1}", e.UserState, e.Result);
}
catch( Exception ex ) {
labMessage.Text = string.Format("{0} => Error: {1}", e.UserState, ex.Message);
}
}
什么,您不认为我直接访问e.Result,不会出现异常吗?
再来看一下,我写的事件参数类型吧,看看我是如何做的:
public class CallCompletedEventArgs : AsyncCompletedEventArgs
{
private TOut _result;
public CallCompletedEventArgs(TOut result, Exception e, bool canceled, object state)
: base(e, canceled, state)
{
_result = result;
}
public TOut Result
{
get
{
base.RaiseExceptionIfNecessary();
return _result;
}
}
}
其中,RaiseExceptionIfNecessary()方法的实现如下(微软实现的):
protected void RaiseExceptionIfNecessary()
{
if( this.Error != null ) {
throw new TargetInvocationException(SR.GetString("Async_ExceptionOccurred"), this.Error);
}
if( this.Cancelled ) {
throw new InvalidOperationException(SR.GetString("Async_OperationCancelled"));
}
}
让我们再来看前面的EAP模式中完成事件中的标准处理代码:
void client_OnCallCompleted(object sender, MyAysncClient<string, string>.CallCompletedEventArgs e)
{
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);
}
的确,这种做法对于EAP模式来说:是标准的处理方式:首先要判断this.Error != null ,为什么这个 不规范 的方式会成为标准呢?
我要再问一句:为什么不用try.....catch这种更规范的处理方式呢?
显然,我也演示了:EAP模式在获取结果时,也可以支持try.....catch这种方式的。在这里不用它的理由是因为:
对于if判断这类简单的操作来说,抛异常是个【昂贵】的操作。这种明显可以提高性能的做法,难道有错吗?
在.net设计规范中,还有Tester-Doer, Try-Parse这二类模式。我想很多人也应该用过的吧,设计它们也是因为性能问题,与EAP的理由是一样的。
再来总结一下。我的CallCompletedEventArgs类在实现时,有二个关键点:
1. 事件类型要从AsyncCompletedEventArgs继承。
2. 用只读属性返回结果,但在访问前,要调用基类的base.RaiseExceptionIfNecessary();
这些都是EAP模式中,正确的设计方式。什么是模式?这就是模式。什么是规范?这就是规范!
我们不能因为错误的设计,或者说,不尊守规范的设计,而造成的缺陷也要怪罪于EAP 。
结束语
异步是个很有用的技术,不管是对于桌面程序还是服务程序都是很用意义的。
不过,相对于同步调用来说,异步也是复杂的,但它的各种使用方式也是很精彩的。
如果,您认为阅读这篇博客让您有些收获,不妨点击一下右下角的【推荐】按钮。
如果,您希望更容易地发现我的新博客,不妨点击一下右下角的【关注 Fish Li】。
因为,我的写作热情也离不开您的肯定支持。