在ASP.NET MVC框架中感觉一下回到原始社会中简直和异步页的封装没法比。来看代码吧。注意代码中的注释// 实际可处理的Action名称为 Test1 注意名称后要加上 Async public void Test1Async() { // 告诉ASP.NET MVC要开始一个异步操作了。 AsyncManager.OutstandingOperations.Increment(); string str Guid.NewGuid().ToString(); MyAysncClientstring, string client new MyAysncClientstring, string(ServiceUrl); client.OnCallCompleted new MyAysncClientstring, string.CallCompletedEventHandler(client_OnCallCompleted); client.CallAysnc(str, str); // 开始异步调用 } void client_OnCallCompleted(object sender, MyAysncClientstring, 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(); MyAysncClientstring, string client new MyAysncClientstring, string(ServiceUrl); client.UserData result1; client.OnCallCompleted new MyAysncClientstring, string.CallCompletedEventHandler(client2_OnCallCompleted); client.CallAysnc(str, str); // 开始第一个异步任务 string str2 T2_ Guid.NewGuid().ToString(); MyAysncClientstring, string client2 new MyAysncClientstring, string(ServiceUrl); client2.UserData result2; client2.OnCallCompleted new MyAysncClientstring, string.CallCompletedEventHandler(client2_OnCallCompleted); client2.CallAysnc(str2, str2); // 开始第二个异步任务 string str3 T3_ Guid.NewGuid().ToString(); MyAysncClientstring, string client3 new MyAysncClientstring, string(ServiceUrl); client3.UserData result3; client3.OnCallCompleted new MyAysncClientstring, string.CallCompletedEventHandler(client2_OnCallCompleted); client3.CallAysnc(str3, str3); // 开始第三个异步任务 } void client2_OnCallCompleted(object sender, MyAysncClientstring, string.CallCompletedEventArgs e) { // 递减内部的异步任务累加器。有点类似AspNetSynchronizationContext的设计。 AsyncManager.OutstandingOperations.Decrement(); MyAysncClientstring, string client (MyAysncClientstring, 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, MyAysncClientstring, 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, MyAysncClientstring, 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, MyAysncClientstring, 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 。