详解ASP.NET各种异步操作(第1页)
文章来源: 更新时间:2011/12/3 捷迅软件网

    今天我再来谈异步,专门来谈在ASP.NET平台下的各种异步操作。在这篇文章中,我主要演示在ASP.NET中如何使用各种异步操作。

    在上篇博客【C#客户端的异步操作】,我介绍了一些。NET中实现异步操作的方法,在那篇博客中,我是站在整个。NET平台的角度来讲述各种异步操作的实现方式,并针对各种异步操作以及不同的编程模型给出了一些参考建议。上篇博客谈到的内容可以算是异步操作的基础,今天我再来谈异步,专门来谈在ASP.NET平台下的各种异步操作。在这篇博客中,我主要演示在ASP.NET中如何使用各种异步操作。

    在后续博客中,我还会分析ASP.NET的源码,解释为什么可以这样做,或者这样的原因是什么,以解密内幕的方式向您解释这些操作的实现原理。

    由于本文是【C#客户端的异步操作】的续集,因此一些关于异步的基础内容,就不再过多解释了。如不理解本文的示例代码,请先看完那篇博文吧。

    在【C#客户端的异步操作】的结尾,有一个小节【在Asp.NET中使用异步】,我把我上次写好的示例做了个简单的介绍,今天我来专门解释那些示例代码。不过,在写博客的过程中,又做了一点补充,所以,请以前下载过示例代码的朋友,你们需要重新下载那些示例代码(还是那篇博客中)。说明:那些代码都是在示范使用异步的方式调用【用Asp.NET写自己的服务框架】博客中所谈到的那个服务框架,且服务方法的代码为:

    [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());

    }

    在ASP.NET中使用异步

    我在【C#客户端的异步操作】中提到一个观点: 对于服务程序而言,异步处理可以提高吞吐量。什么是服务程序,简单说来就是:可以响应来自网络请求的服务端程序。我们熟悉的ASP.NET显然是符合这个定义的。因此在ASP.NET程序中,适当地使用异步是可以提高服务端吞吐量的。这里所说的适当地使用异步,一般是说:当服务器的压力不大且很多处理请求的执行过程被阻塞在各种I/O等待(以网络调用为主)操作上时,而采用异步来减少阻塞工作线程的一种替代同步调用的方法。反之,如果服务器的压力已经足够大,或者没有发生各种I/O等待,那么,在此情况下使用异步是没有意义的。

    在。NET中,几乎所有的服务编程模型都是采用线程池处理请求任务的多线程工作模式。自然地,ASP.NET也不例外,根据【C#客户端的异步操作】的分析,我们就不能再使用一些将阻塞操作交给线程池的方法了。比如:委托的异步调用,直接使用线程池,都是不可取的。直接创建线程也是不合适的,因此那种方式会随着处理请求的数量增大而创建一大堆线程,最后也将会影响性能。因此,最终能被选用的只用BeginXxxxx/EndXxxxx方式。不过,我要补充的是:还有基于事件通知的异步模式也是一个不错的选择(我会用代码来证明),只要它是对原始BeginXxxxx/EndXxxxx方式的包装。

    在【用Asp.NET写自己的服务框架】中,我说过,ASP.NET处理请求是采用了一种被称为【管线】的方式,管线由HttpApplication控制并引发的一系列事件,由HttpHandler来处理请求,而HttpModule则更多地是一种辅助角色。还记得我在【C#客户端的异步操作】 总结的异步特色吗:【一路异步到底】。 ASP.NET的处理过程要经过它们的处理,自然它们对于请求的处理也必须要支持异步。幸运地是,这些负责请求处理的对象都是支持异步的。今天的博客也将着重介绍它们的异步工作方式。

    WebForm框架,做为ASP.NET平台上最主要且默认的开发框架,我自然也会全面地介绍它所支持的各种异步方式。MVC框架从2.0开始,也开始支持异步,本文也会介绍如何在这个版本中使用异步。

    该选哪个先出场呢?我想了很久,最后还是决定先请出处理请求的核心对象:HttpHandler 。

    异步 HttpHandler

    关于HttpHandler的接口,我在【用Asp.NET写自己的服务框架】中已有介绍,这里就不再贴出它的接口代码了,只想说一句:那是个同步调用接口,它并没有异步功能。要想支持异步,则必须使用另一个接口:IHttpAsyncHandler

    // 摘要:

    //     定义 HTTP 异步处理程序对象必须实现的协定。

    public interface IHttpAsyncHandler : IHttpHandler

    {

    // 摘要:

    //     启动对 HTTP 处理程序的异步调用。

    //

    // 参数:

    //   context:

    //     一个 System.Web.HttpContext 对象,该对象提供对用于向 HTTP 请求提供服务的内部服务器对象(如 Request、Response、Session

    //     和 Server)的引用。

    //

    //   extraData:

    //     处理该请求所需的所有额外数据。

    //

    //   cb:

    //     异步方法调用完成时要调用的 System.AsyncCallback。如果 cb 为 null,则不调用委托。

    //

    // 返回结果:

    //     包含有关进程状态信息的 System.IAsyncResult。

    IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData);

    //

    // 摘要:

    //     进程结束时提供异步处理 End 方法。

    //

    // 参数:

    //   result:

    //     包含有关进程状态信息的 System.IAsyncResult。

    void EndProcessRequest(IAsyncResult result);

    }

    这个接口也很简单,只有二个方法,并且与【C#客户端的异步操作】 提到的BeginXxxxx/EndXxxxx设计方式差不多。如果这样想,那么后面的事件就好理解了。在。NET中,异步都是建立在IAsyncResult接口之上的,而BeginXxxxx/EndXxxxx是对这个接口最直接的使用方式。

    下面我们来看一下如何创建一个支持异步的ashx文件(注意:代码中的注释很重要)。

    public class AsyncHandler : IHttpAsyncHandler {

    private static readonly string ServiceUrl = "http://localhost:22132/service/DemoService/CheckUserLogin";

    public void ProcessRequest(HttpContext context)

    {

    // 注意:这个方法没有必要实现。因为根本就不调用它。

    // 但要保留它,因为这个方法也是接口的一部分。

    throw new NotImplementedException();

    }

    public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)

    {

    // 说明:

    //   参数cb是一个ASP.NET的内部委托,EndProcessRequest方法将在那个委托内部被调用。

    LoginInfo info = new LoginInfo();

    info.Username = context.Request.Form["Username"];

    info.Password = context.Request.Form["Password"];

    MyHttpClient<LoginInfo, string> http = new MyHttpClient<LoginInfo, string>();

    http.UserData = context;

    // ================== 开始异步调用 ============================

    // 注意:您所需要的回调委托,ASP.NET已经为您准备好了,直接用cb就好了。

    return http.BeginSendHttpRequest(ServiceUrl, info, cb, http);

    // ==============================================================

    }

    public void EndProcessRequest(IAsyncResult ar)

    {

    MyHttpClient<LoginInfo, string> http = (MyHttpClient<LoginInfo, string>)ar.AsyncState;

    HttpContext context = (HttpContext)http.UserData;

    context.Response.ContentType = "text/plain";

    context.Response.Write("AsyncHandler Result: ");

    try {

    // ============== 结束异步调用,并取得结果 ==================

    string result = http.EndSendHttpRequest(ar);

    // ==============================================================

    context.Response.Write(result);

    }

    catch( System.NET.WebException wex ) {

    context.Response.StatusCode = 500;

    context.Response.Write(HttpWebRequestHelper.SimpleReadWebExceptionText(wex));

    }

    catch( Exception ex ) {

    context.Response.StatusCode = 500;

    context.Response.Write(ex.Message);

    }

    }

    实现其实是比较简单的,大致可以总结如下:

    1. 在BeginProcessRequest()方法,调用要你要调用的异步开始方法,通常会是另一个BeginXxxxx方法。

    2. 在EndProcessRequest()方法,调用要你要调用的异步结束方法,通常会是另一个EndXxxxx方法。

    真的就是这么简单。

    这里要说明一下,在【C#客户端的异步操作】中,我演示了如何使用。NET framework中的API去实现完整的异步发送HTTP请求的调用过程,但那个过程需要二次异步,而这个IHttpAsyncHandler接口却只支持一次回调。因此,对于这种情况,就需要我们自己封装,将多次异步转变成一次异步。以下是我包装的一次异步的简化版本:

    下面这个包装类非常有用,我后面的示例还将会使用它。它也示范了如何创建自己的IAsyncResult封装。因此建议仔细阅读它。(注意:代码中的注释很重要)

    /// <summary>

    /// 对异步发送HTTP请求全过程的包装类,

    /// 按IAsyncResult接口要求提供BeginSendHttpRequest/EndSendHttpRequest方法(一次回调)

    /// </summary>

    /// <typeparam name="TIn"></typeparam>

    /// <typeparam name="TOut"></typeparam>

    public class MyHttpClient<TIn, TOut>

    {

    /// <summary>

    /// 用于保存额外的用户数据。

    /// </summary>

    public object UserData;

    public IAsyncResult BeginSendHttpRequest(string url, TIn input, AsyncCallback cb, object state)

    {

    // 准备返回值

    MyHttpAsyncResult ar = new MyHttpAsyncResult(cb, state);

    // 开始异步调用

    HttpWebRequestHelper<TIn, TOut>.SendHttpRequestAsync(url, input, SendHttpRequestCallback, ar);

    return ar;

    }

    private void SendHttpRequestCallback(TIn input, TOut result, Exception ex, object state)

    {

    // 进入这个方法表示异步调用已完成

    MyHttpAsyncResult ar = (MyHttpAsyncResult)state;

    // 设置完成状态,并发出完成通知。

    ar.SetCompleted(ex, result);

    }

    public TOut EndSendHttpRequest(IAsyncResult ar)

    {

    if( ar == null )

    throw new ArgumentNullException("ar");

    // 说明:我并没有检查ar对象是不是与之匹配的BeginSendHttpRequest实例方法返回的,

    // 虽然这是不规范的,但我还是希望示例代码能更简单。

    // 我想应该极少有人会乱传递这个参数。

    MyHttpAsyncResult myResult = ar as MyHttpAsyncResult;

    if( myResult == null )

    throw new ArgumentException("无效的IAsyncResult参数,类型不是MyHttpAsyncResult。");

    if( myResult.EndCalled )

    throw new InvalidOperationException("不能重复调用EndSendHttpRequest方法。");

    myResult.EndCalled = true;

    myResult.WaitForCompletion();

    return (TOut)myResult.Result;

    }

    }

    internal class MyHttpAsyncResult : IAsyncResult

    {

    internal MyHttpAsyncResult(AsyncCallback callBack, object state)

    {

    _state = state;

    _asyncCallback = callBack;

    }

    internal object Result { get; private set; }

    internal bool EndCalled;

    private object _state;

    private volatile bool _isCompleted;

    private ManualResetEvent _event;

    private Exception _exception;

    private AsyncCallback _asyncCallback;

    public object AsyncState

    {

    get { return _state; }

    }

    public bool CompletedSynchronously

    {

    get { return false; } // 其实是不支持这个属性

    }

    public bool IsCompleted

    {

    get { return _isCompleted; }

    }

    public WaitHandle AsyncWaitHandle

    {

    get {

    if( _isCompleted )

    return null;    // 注意这里并不返回WaitHandle对象。

    if( _event == null )     // 注意这里的延迟创建模式。

    _event = new ManualResetEvent(false);

    return _event;

    }

    }

    internal void SetCompleted(Exception ex, object result)

    {

    this.Result = result;

    this._exception = ex;

    this._isCompleted = true;

    ManualResetEvent waitEvent = Interlocked.CompareExchange(ref _event, null, null);

    if( waitEvent != null )

    waitEvent.Set();        // 通知 EndSendHttpRequest() 的调用者

    if( _asyncCallback != null )

    _asyncCallback(this);    // 调用 BeginSendHttpRequest()指定的回调委托

    }

    internal void WaitForCompletion()

    {

    if( _isCompleted == false ) {

    WaitHandle waitEvent = this.AsyncWaitHandle;

    if( waitEvent != null )

    waitEvent.WaitOne();    // 使用者直接(非回调方式)调用了EndSendHttpRequest()方法。

    }

    if( _exception != null )

    throw _exception;    // 将异步调用阶段捕获的异常重新抛出。

    }

    // 注意有二种线程竞争情况:

    //  1. 在回调线程中调用SetCompleted时,原线程访问AsyncWaitHandle

    //  2. 在回调线程中调用SetCompleted时,原线程调用WaitForCompletion

    // 说明:在回调线程中,会先调用SetCompleted,再调用WaitForCompletion

    }

    对于这个包装类来说,最关键还是MyHttpAsyncResult的实现,它是异步模式的核心

    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);

[1] [2] [3] [4] 下一页

资讯录入:海洋    责任编辑:海洋 
数据载入中,请稍后……