Hello World
Spiga

为ASP.NET MVC扩展异步Action功能(下)

2009-02-04 09:04 by 老赵, 11548 visits

本文分为上下两部分,您也可以从《Extend ASP.NET MVC for Asynchronous Action》获得全部内容。

执行Action方法

对于执行同步Action的SyncMvcHandler,其实现十分简单而直接:

public class SyncMvcHandler : IHttpHandler, IRequiresSessionState
{
    public SyncMvcHandler(
        IController controller,
        IControllerFactory controllerFactory,
        RequestContext requestContext)
    {
        this.Controller = controller;
        this.ControllerFactory = controllerFactory;
        this.RequestContext = requestContext;
    }

    public IController Controller { get; private set; }
    public RequestContext RequestContext { get; private set; }
    public IControllerFactory ControllerFactory { get; private set; }

    public virtual bool IsReusable { get { return false; } }

    public virtual void ProcessRequest(HttpContext context)
    {
        try
        {
            this.Controller.Execute(this.RequestContext);
        }
        finally
        {
            this.ControllerFactory.ReleaseController(this.Controller);
        }
    }
}

而对于异步Action,我之前一直思考着怎么将框架的默认实现,也就是单个方法调用,转化成两个方法(BeginXxx/EndXxx)调用。曾经我想过自己实现一个新的ActionInvoker,但是这就涉及到了大量的工作,尤其是如果希望保持框架现有的功能(ActionFilter,ActionSelector等等),最省力的方法可能就是继承ControllerActionInvoker,并设法使用框架已经实现的各种辅助方法。但是在分析了框架代码之后我发现复用也非常困难,举例来说,ControllerActionInvoker判定一个方法为Action的依据之一是这个方法返回的是ActionResult类型或其子类,这意味着我无法直接使用这个方法来获取一个返回IAsyncResult的BeginXxx方法;同理,对于查找EndXxx方法,我可能需要在请求名为Abc的异步Action时,将EndAbc作为查找依据交由现成的方法来查询——但是,如果又有一个请求是直接针对一个名为EndAbc的同步Action的那又怎么办呢?

由于这些问题存在,我在去年设法实现异步Action时几乎重写了整个ActionInvoker——其复杂程度可见一斑。而且那个实现对于一些特殊情况的处理依旧不甚友好,需要开发人员在一定程度上做出妥协。这个实现在TechED 2008 China的Session中公布时我就承认它并不能让我满意,建议大家不要将其投入生产环境中。而现在的实现,则非常顺利地解决了整个问题。虽然从理论上讲还不够“完美”,虽然还做出了一些让步。

带来如此多问题的原因就在于我们在设法颠覆框架内部的关键性设计,也就是从单一的Action方法调用,转变为“符合APM的”二段式调用。等等,您是否感觉到了解决问题的关键?没错,那就是“符合APM的”。APM要求我们将一个行为分为BeginXxx和EndXxx两个方法,可是既然ASP.NET MVC框架只能让我们返回一个ActionResult对象……那么我们为什么不在这个对象里包含方法的引用——也就是一个委托对象呢?这虽然不符合正统的APM签名,但是完全可行,不是吗?

public class AsyncActionResult : ActionResult
{
    public AsyncActionResult(
        IAsyncResult asyncResult,
        Func<IAsyncResult, ActionResult> endDelegate)
    {
        this.AsyncResult = asyncResult;
        this.EndDelegate = endDelegate;
    }

    public IAsyncResult AsyncResult { get; private set; }

    public Func<IAsyncResult, ActionResult> EndDelegate { get; private set; }

    public override void ExecuteResult(ControllerContext context)
    {
        context.Controller
            .SetAsyncResult(this.AsyncResult)
            .SetAsyncEndDelegate(this.EndDelegate);
    }
}

由于在Action方法中可以调用BeginXxx方法,我们在AsyncActionResult中只需保留Begin方法返回的IAsyncResult,以及另一个对于EndXxx方法的引用。在AsyncActionResult的ExecuteResult方法中将会保存这两个对象,以便在AsyncMvcHandler的EndProcessRequest方法中重新获取并使用。根据“惯例”,我们还需要定义一个扩展方法,方便开发人员在Action方法中返回一个AsyncActionResult。具体实现非常容易,在这里就展示一下异步Action的编写方式:

[AsyncAction]
public ActionResult AsyncAction(AsyncCallback asyncCallback, object asyncState)
{
    SqlConnection conn = new SqlConnection("...;Asynchronous Processing=true");
    SqlCommand cmd = new SqlCommand("WAITFOR DELAY '00:00:03';", conn);
    conn.Open();

    return this.Async(
        cmd.BeginExecuteNonQuery(asyncCallback, asyncState),
        (ar) =>
        {
            int value = cmd.EndExecuteNonQuery(ar);
            conn.Close();
            return this.View();
        });
}

至此,似乎AsyncMvcHandler也无甚秘密可言了:

public class AsyncMvcHandler : IHttpAsyncHandler, IRequiresSessionState
{
    public AsyncMvcHandler(
        Controller controller,
        IControllerFactory controllerFactory,
        RequestContext requestContext)
    {
        this.Controller = controller;
        this.ControllerFactory = controllerFactory;
        this.RequestContext = requestContext;
    }

    public Controller Controller { get; private set; }
    public RequestContext RequestContext { get; private set; }
    public IControllerFactory ControllerFactory { get; private set; }
    public HttpContext Context { get; private set; }

    public IAsyncResult BeginProcessRequest(
        HttpContext context,
        AsyncCallback cb,
        object extraData)
    {
        this.Context = context;
        this.Controller.SetAsyncCallback(cb).SetAsyncState(extraData);

        try
        {
            (this.Controller as IController).Execute(this.RequestContext);
            return this.Controller.GetAsyncResult();
        }
        catch
        {
            this.ControllerFactory.ReleaseController(this.Controller);
            throw;
        }
    }

    public void EndProcessRequest(IAsyncResult result)
    {
        try
        {
            HttpContext.Current = this.Context;
            ActionResult actionResult = this.Controller.GetAsyncEndDelegate()(result);
            if (actionResult != null)
            {
                actionResult.ExecuteResult(this.Controller.ControllerContext);
            }
        }
        finally
        {
            this.ControllerFactory.ReleaseController(this.Controller);
        }
    }
}

在BeginProcessRequest方法中将保存当前Context——这点很重要,HttpContext.Current是基于CallContext的,一旦经过一次异步回调HttpContext.Current就变成了null,我们必须重设。接着将接收到的AsyncCallback和AsyncState保留,并使用框架中现成的Execute方法执行控制器。当Execute方法返回时一整个Action方法的调用流程已经结束,这意味着其调用结果——即IAsyncResult和EndDelegate对象已经保留。于是将IAsyncResult对象取出并返回。至于EndProcessRequest方法,只是将BeginProcessRequest方法中保存下来的EndDelegate取出,调用,把得到的ActionResult再执行一遍即可。

以上的代码只涉及到普通情况下的逻辑,而在完整的代码中还会包括对于Action方法被某个Filter终止或替换等特殊情况下的处理。此外,无论在BeginProcessRequest还是EndProcessRequest中都需要对异常进行合适地处理,使得Controller Factory能够及时地对Controller对象进行释放。

ModelBinder支持

其实您到目前为止还不能使用异步Action,因为您会发现方法的AsyncCallback参数得到的永远是null。这是因为默认的Model Binder无法得知如何从一个上下文环境中得到一个AsyncCallback对象。这一点倒非常简单,我们只需要构造一个AsyncCallbackModelBinder,而它的BindModel方法仅仅是将AsyncMvcHandler.BeginProcessRequest方法中保存的AsyncCallback对象取出并返回:

public sealed class AsyncCallbackModelBinder : IModelBinder
{
    public object BindModel(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        return controllerContext.Controller.GetAsyncCallback();
    }
}

其使用方式,便是在应用程序启动时将其注册为AsyncCallback类型的默认Binder:

protected void Application_Start()
{
    RegisterRoutes(RouteTable.Routes);
    ModelBinders.Binders[typeof(AsyncCallback)] = new AsyncCallbackModelBinder();
}

对于asyncState参数您也可以使用类似的做法,不过这似乎有些不妥,因为object类型实在过于宽泛,并不能明确代指asyncState参数。事实上,即使您不为asyncState设置binder也没有太大问题,因为对于一个异步ASP.NET请求来说,其asyncState永远是null。如果您一定要指定一个binder,我建议您在每个Action方法的asyncState参数上标记如下的Attribute,它和AsyncStateModelBinder也已经被一并建入项目中了:

[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
public sealed class AsyncStateAttribute : CustomModelBinderAttribute
{
    private static AsyncStateModelBinder s_modelBinder = new AsyncStateModelBinder();

    public override IModelBinder GetBinder()
    {
        return s_modelBinder;
    }
}

使用方式如下:

[AsyncAction]
public ActionResult AsyncAction(AsyncCallback cb, [AsyncState]object state) { ... }

其实,基于Controller的扩展方法GetAsyncCallback和GetAsyncState均为公有方法,您也可以让Action方法不接受这两个参数而直接从Controller中获取——当然这种做法降低了可测试性,不值得提倡。

限制和缺点

如果这个解决方案没有缺陷,那么相信它已经被放入ASP.NET MVC 1.0中,而轮不到我在这里扩展一番了。目前的这个解决方案至少有以下几点不足:

  1. 没有严格遵守.NET中的APM模式,虽然不影响功能,但这始终是一个遗憾。
  2. 由于利用了框架中的现成功能,所有的Filter只能运行在BeginXxx方法上。
  3. 由于EndXxx方法和最终ActionResult的执行都没有Filter支持,因此如果在这个过程中抛出了异常,将无法进入ASP.NET MVC建议的异常处理功能中。

根据ASP.NET MVC框架的Roadmap,ASP.NET MVC框架1.0之后的版本中将会支持异步Action,相信以上这些缺陷到时候都能被弥补。不过这就需要大量的工作,这只能交给ASP.NET MVC团队去慢慢执行了。事实上,您现在已经可以在ASP.NET MVC RC源代码的MvcFutures项目中找到异步Action处理的相关内容。它添加了IAsyncController,AsyncController,IAsyncActionInvoker,AsyncControllerActionInvoker等许多扩展。虽说它们都“继承”了现有的类,但是与我之前的判断相似,如AsyncControllerActionInvoker几乎完全重新实现了一遍ActionInvoker中的各种功能——我还没有仔细阅读代码,因此无法判断出这种设计是否优秀,只希望它能像ASP.NET MVC本身那样的简单和优雅。

接下来,我打算为现在的代码的EndXxx方法也加上Filter支持,我需要仔细阅读ASP.NET MVC的源代码来寻找解决方案。希望它能够成为ASP.NET MVC正式支持异步Action之前较好的替代方案。

更多资料

完整的项目代码已经放置在MSDN Code Gallery中,您可以在这里访问到关于它的“性能测试”等更多信息。这篇文章着重讲解了扩展的设计原理,省略了涉及特殊状况处理以及程序健壮性等实现细节的描述,欢迎您下载代码并提出改进建议。

相关文章

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

23 条回复

  1. 重典
    *.*.*.*
    链接

    重典 2009-02-04 08:46:00

    快枪赵

  2. 伯乐族人[未注册用户]
    *.*.*.*
    链接

    伯乐族人[未注册用户] 2009-02-04 09:02:00

    你的文章总是很经典,伯乐族 www.*** 收藏了。

  3. 老赵
    admin
    链接

    老赵 2009-02-04 09:04:00

    --引用--------------------------------------------------
    重典: 快枪赵
    --------------------------------------------------------
    这不是骂我吗?

  4. 幸运草
    *.*.*.*
    链接

    幸运草 2009-02-04 09:15:00

    也来顶一个,应该是“快抢”吧

  5. 阿不
    *.*.*.*
    链接

    阿不 2009-02-04 09:17:00

    什么是:APM模式

  6. 老赵
    admin
    链接

    老赵 2009-02-04 09:21:00

    @阿不
    我难道没有说吗?其实在(上)里面说过了,而且完全可以去搜索一下吧。

  7. Angel Lucifer
    *.*.*.*
    链接

    Angel Lucifer 2009-02-04 10:47:00

    @重典
    牛年牛帖。笑死我了,哈哈。

    @Jeffrey Zhao
    如果注重性能的话,是否完全遵循 APM 模式不重要。
    APM 只是简化了下异步操作的麻烦步骤,它实际上对高性能还是有害的。因为针对每一次操作,都必须创建一个 IAsyncResult 对象,并且这玩意不能被重复使用。再就是由于大量 new 来 dispose 掉,无疑会增大 GC 压力。这些都是影响性能的因素。

  8. 老赵
    admin
    链接

    老赵 2009-02-04 10:57:00

    @Angel Lucifer
    使用异步是为了增加吞吐量。
    还有就是想当然了哦,创建IAsyncResult没有什么关系,创建的对象好多好多,这么一个小小的东西根本不是问题。而且生命那么短期的对象,不会对GC有影响。
    异步机制很有效,只要其他服务性能不会成为瓶颈,可以说吞吐量一下子就上去了——只是编程麻烦。这一点倒不是asp.net特有的,建议可以看看这篇,看看淘宝是怎么利用web异步方式来提高系统响应能力和容错性的。
    http://www.infoq.com/cn/articles/request-asyn-risk-reduce

  9. andy.wu
    *.*.*.*
    链接

    andy.wu 2009-02-04 11:05:00

    @Angel Lucifer

    性能这个术语实在是有点模糊。比如采用APM模式使服务器的负载提高,也可称为提高了服务器的性能,不是吗。:)

    ps: 发现术语有时候真是害人啊。术语的作用就是将大量很简单的事情抽象成一个单词,然后大多数不知道该术语的人都十分景仰,而在知道后大呼上当,进而拿出去吓别人,还会和同样知道这个术语的人有知己之感,哈哈。

  10. Angel Lucifer
    *.*.*.*
    链接

    Angel Lucifer 2009-02-04 11:18:00

    @Jeffrey Zhao
    咳,你想到哪去了,呵呵。

    我的回复主要针对这句:
    “没有严格遵守.NET中的APM模式,虽然不影响功能,但这始终是一个遗憾。”

    我并不反对使用异步方式。相反,我认为能使用异步就尽量使用异步。它不但提升吞吐量,就是对提升程序性能,提升程序可伸缩性上也很有帮助。

    但是使用异步有很多种方式,并不一定非得局限于 .NET 提供的 APM 模式。它只是 Microsoft 为我们提供的异步方式之一(非常方便的方式),前面的回复专门阐述了这种模式的缺点以及产生这种缺点的原因,当然它的好处也显而易见。

    至于这种方式是否会对 GC 产生压力。小规模流量自然影响不大,但是针对大规模流量来说,对于 GC 的影响已经到了不可忽视的地步。.NET Framework 3.5 中 Socket 异步方式在原有 APM 模式之外,专门针对这种情况提出了另外一种模式。 此外,不能重复使用 IAsyncResult 对象的坏处就不说了,连火星人都知道了,呵呵。

  11. Angel Lucifer
    *.*.*.*
    链接

    Angel Lucifer 2009-02-04 11:33:00

    @andy.wu
    对于术语,我倒是有不同的意见,拿出来探讨一下,呵呵。

    我认为,术语大部分都有很明确的解释。只不过太多人不太明白或者不深究,以至于滥用,误用,套用的情况比较多,结果把大家都搞混了。

    比方说,性能。它指的就是单一机器上,在单位时间内程序运行的快慢。
    再比如老赵刚在回复里提到的吞吐量(率),这是另外层次上的术语。这个就跟老兄说的负载搭上边了。这俩个表达的内涵差不多。指的是服务器承受连接以及能够快速响应连接的能力。
    在实际中,一般提升负载的同时,性能随之下降的。也可能我孤陋寡闻,还没有见过例外的情况。

    PS: “风险投资”的经典见解 :
    所谓风险投资,就是越有风险越投资,没有风险绝不投资。 -- From 《非诚勿扰》

  12. 重典
    *.*.*.*
    链接

    重典 2009-02-04 13:41:00

    @Jeffrey Zhao
    解释权归我所有
    所谓快枪赵,是某牛代称,一般尿性(东北方言,NB的意思)的人都是 职称+姓
    比如神剪张,风筝黄...


    以上纯属恶搞...不要追杀我啊

    老赵的确是手快啊.还有英文的一下子出两份

  13. 老赵
    admin
    链接

    老赵 2009-02-04 13:46:00

    @重典
    还好……之前放假了一个星期不是么……

  14. Angel Lucifer
    *.*.*.*
    链接

    Angel Lucifer 2009-02-04 14:18:00

    @重典
    快枪赵真是生动形象啊,您老是语言大师,^_^

  15. daconglee
    *.*.*.*
    链接

    daconglee 2009-02-04 20:01:00

    老赵你的blog有什么特殊设置吗?我使用Maxthon,在1280X800的分辨率下看,总有个横向的滚动条,看东西很不方便,不知道其他人有这问题吗?

  16. 老赵
    admin
    链接

    老赵 2009-02-04 21:53:00

    --引用--------------------------------------------------
    daconglee: 老赵你的blog有什么特殊设置吗?我使用Maxthon,在1280X800的分辨率下看,总有个横向的滚动条,看东西很不方便,不知道其他人有这问题吗?
    --------------------------------------------------------
    因为代码用了pre,把页面撑宽了

  17. 老赵
    admin
    链接

    老赵 2009-02-04 21:56:00

    @海洋——海纳百川,有容乃大.
    Scott的blog都只能算是些新闻和入门材料,其实并没有多大价值和技术含量。
    有时候看Scott的blog要看下面的评论比较好,然后再加上在社区里和别人的讨论,才有价值。

  18. 假如爱有天意
    *.*.*.*
    链接

    假如爱有天意 2009-02-05 14:45:00

    无敌了。。。很难很抽象。

  19. ITAres
    *.*.*.*
    链接

    ITAres 2009-02-25 18:46:00

    我没发现这样做有什么好处?


    好像用户请求等待的时间跟同步处理是一样的...


    老赵可以说说这样做的优点在什么地方吗?主要解决了什么问题?

  20. 一箭
    *.*.*.*
    链接

    一箭 2009-03-08 14:16:00

    听说R2中有默认的异步支持,不知能否介绍有关这方面的信息.
    另外,想请教一下,关于WebService有没有类似的处理.

  21. 老赵
    admin
    链接

    老赵 2009-03-08 15:24:00

    @一箭
    是V2不是R2,呵呵。
    不知道你说的是ASP.NET AJAX的Web Service还是其他Web Service。
    前者没有,后者有。

  22. 一箭
    *.*.*.*
    链接

    一箭 2009-03-08 20:45:00

    我指的是后者,不知能给一个介绍这方面的资料链接地址吗?

  23. 问题多多[未注册用户]
    *.*.*.*
    链接

    问题多多[未注册用户] 2009-11-05 16:45:00

    你好 我想问一个问题
    现在我有个需求就是 在controller A里边的一个index action 通过业务逻辑处理后,我需要在 controller B里边某个action对应的页面上显示数据,这个如何实现呢?不希望通过跳转action 的方法实现 能通过更改视图引擎实现吗?

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我