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不知道要创建多少个对象,我真的数不清楚。

  1. Jeffer Richter说:如果在登记事件处理方法之前调用XxxAsync方法,......。看到这里,我笑了。 显然,大牛是非常讨厌EAP模式的。EAP是使用了事件,这个错误的调用顺序问题如果是EAP的错,那么.NET的事件模式就是个错误的设计。 大牛说这句真是不负责任嘛。

  2. 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 。

相关推荐
雨师@1 小时前
go语言项目--实例化(图书管理)--006
开发语言·后端·golang
kuro-shiro1 小时前
SpringBoot 启动流程
java·spring boot·后端
独孤九剑打醒他10 小时前
双层Master-Worker软硬协同调度架构:从根源解决分布式数据一致性难题
后端·嵌入式硬件·硬件架构·硬件工程
不会c+12 小时前
02-SpringBoot配置文件
java·spring boot·后端
雨辰AI13 小时前
生产级实战:人大金仓 V9 标准化运维手册(日常巡检 + 监控告警 + 应急处置)
java·运维·数据库·后端
TeamDev14 小时前
JxBrowser 9.3.0 版本发布啦!
java·后端·c#·混合应用·jxbrowser·浏览器控件·异步媒体设备
陈随易14 小时前
Rust、Golang、MoonBit 编译成 WASM,体积和速度差距有多大?
前端·后端·程序员
IT_陈寒14 小时前
Python多线程的坑,我居然现在才踩到
前端·人工智能·后端
魏祖潇15 小时前
DDD 完整指南——AI 时代工程师的第一道秩序分水岭
人工智能·后端