• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    迪恩网络公众号

细说ASP.NET的各种异步操作(转自fishli)

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

在上篇博客【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());
}

我在【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);

其中BeginEventHandler与EndEventHandler的定义如下:

// 摘要:
//     表示处理异步事件(如应用程序事件)的方法。此委托在异步操作开始时调用。
//
// 返回结果:
//     System.IAsyncResult,它表示 System.Web.BeginEventHandler 操作的结果。
public delegate IAsyncResult BeginEventHandler(object sender, EventArgs e, AsyncCallback cb, object extraData);

// 摘要:
//     表示处理异步事件(如应用程序事件)的方法。
public delegate void EndEventHandler(IAsyncResult ar);

如果单看以上接口的定义,可以发现除了“object sender, EventArgs e”是多余部分之外,其余部分则刚好与Begin/End的设计方式完全吻合,没有一点多余。

我们来看一下如何调用这个方法来实现异步的操作:(注意代码中的注释)

protected void button1_click(object sender, EventArgs e)
{
    Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());

    // 准备回调数据,它将由AddOnPreRenderCompleteAsync的第三个参数被传入。
    MyHttpClient<string, string> http = new MyHttpClient<string, string>();
    http.UserData = textbox1.Text;

    // 注册一个异步任务。注意这三个参数哦。
    AddOnPreRenderCompleteAsync(BeginCall, EndCall, http);
}

private IAsyncResult BeginCall(object sender, EventArgs e, AsyncCallback cb, object extraData)
{
    // 在这个方法中,
    // sender 就是 this
    // e 就是 EventArgs.Empty
    // cb 就是 EndCall
    // extraData 就是调用AddOnPreRenderCompleteAsync的第三个参数
    Trace.Write("BeginCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());

    MyHttpClient<string, string> http = (MyHttpClient<string, string>)extraData;
    
    // 开始一个异步调用。页面线程也最终在执行这个调用后返回线程池了。
    // 中间则是等待网络的I/O的完成通知。
    // 如果网络调用完成,则会调用 cb 对应的回调委托,其实就是下面的方法
    return http.BeginSendHttpRequest(ServiceUrl, (string)http.UserData, cb, http);
}

private void EndCall(IAsyncResult ar)
{
    // 到这个方法中,表示一个任务执行完毕。
    // 参数 ar 就是BeginCall的返回值。

    Trace.Write("EndCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());

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

    try{
        // 结束异步调用,获取调用结果。如果有异常,也会在这里抛出。
        string result = http.EndSendHttpRequest(ar);
        labMessage.Text = string.Format("{0} => {1}", str, result);
    }
    catch(Exception ex){
        labMessage.Text = string.Format("{0} => Error: {1}", str, ex.Message);
    }
}

对照一下异步HttpHandler中的介绍,你会发现它们非常像。

如果要执行多个异步任务,可以参考下面的代码:

protected void button1_click(object sender, EventArgs e)
{
    Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());

    MyHttpClient<string, string> http = new MyHttpClient<string, string>();
    http.UserData = textbox1.Text;
    AddOnPreRenderCompleteAsync(BeginCall, EndCall, http);


    MyHttpClient<string, string> http2 = new MyHttpClient<string, string>();
    http2.UserData = "T2_" + Guid.NewGuid().ToString();
    AddOnPreRenderCompleteAsync(BeginCall2, EndCall2, http2);
}

也很简单,就是调用二次AddOnPreRenderCompleteAsync而已。

前面我说过,异步的处理是发生在PreRender和PreRenderComplete之间,我们来还是看一下到底是不是这样的。 在ASP.NET的Page中,我们很容易的输出一些调试信息,且它们会显示在所处的页面生命周期的相应执行阶段中。 这个方法很简单,在Page指令中加上【Trace="true"】选项,并在页面类的代码中调用Trace.Write()或者Trace.Warn()就可以了。 下面来看一下我加上调试信息的页面执行过程吧。

从这张图片中,我们至少可以看到二个信息:
1. 所有的异步任务的执行过程确实发生在PreRender和PreRenderComplete之间。
2. 所有的异步任务被串行地执行了。

2. 调用Page.RegisterAsyncTask()的异步页

我一直认为ASP.NET程序也是一种服务程序,它要对客户端浏览器发出的请求而服务。 由于是服务,对于要服务的对象来说,都希望能尽快地得到响应,这其实也是对服务的一个基本的要求, 那就是:高吞量地快速响应。

对于前面所说的方法,显然,它的所有异步任务都是串行执行的,对于客户端来说,等待的时间会较长。 而且,最严重的是,如果服务超时,上面的方法会一直等待,直到本次请求超时。 为了解决这二个问题,ASP.NET定义了一种异步任务类型:PageAsyncTask 。它可以解决以上二种问题。 首先我们还是来看一下PageAsyncTask类的定义:(说明:这个类的关键就是它的构造函数)

// 摘要:
//     使用并行执行的指定值初始化 System.Web.UI.PageAsyncTask 类的新实例。
//
// 参数:
//   state:
//     表示任务状态的对象。
//
//   executeInParallel:
//     指示任务能否与其他任务并行处理的值。
//
//   endHandler:
//     当任务在超时期内成功完成时要调用的处理程序。
//
//   timeoutHandler:
//     当任务未在超时期内成功完成时要调用的处理程序。
//
//   beginHandler:
//     当异步任务开始时要调用的处理程序。
//
// 异常:
//   System.ArgumentNullException:
//     beginHandler 参数或 endHandler 参数未指定。
public PageAsyncTask(BeginEventHandler beginHandler, EndEventHandler endHandler, 
			EndEventHandler timeoutHandler, object state, bool executeInParallel);

注意这个构造函数的签名,它与AddOnPreRenderCompleteAsync()相比,多了二个参数:EndEventHandler timeoutHandler, bool executeInParallel 。 它们的含义上面的注释中有说明,这里只是提示您要注意它们而已。

创建好一个PageAsyncTask对象后,只要调用页面的RegisterAsyncTask()方法就可以注册一个异步任务。 具体用法可参考我的如下代码:(注意代码中的注释)

protected void button1_click(object sender, EventArgs e)
{
    Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());

    // 准备回调数据,它将由PageAsyncTask构造函数的第四个参数被传入。
    MyHttpClient<string, string> http = new MyHttpClient<string, string>();
    http.UserData = textbox1.Text;

    // 创建异步任务
    PageAsyncTask task = new PageAsyncTask(BeginCall, EndCall, TimeoutCall, http);
    // 注册异步任务
    RegisterAsyncTask(task);
}

private IAsyncResult BeginCall(object sender, EventArgs e, AsyncCallback cb, object extraData)
{
    // 在这个方法中,
    // sender 就是 this
    // e 就是 EventArgs.Empty
    // cb 是ASP.NET定义的一个委托,我们只管在异步调用它时把它用作回调委托就行了。
    // extraData 就是PageAsyncTask构造函数的第四个参数
    Trace.Warn("BeginCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());

    MyHttpClient<string, string> http = (MyHttpClient<string, string>)extraData;

    // 开始一个异步调用。
    return http.BeginSendHttpRequest(ServiceUrl, (string)http.UserData, cb, http);
}

private void EndCall(IAsyncResult ar)
{
    // 到这个方法中,表示一个任务执行完毕。
    // 参数 ar 就是BeginCall的返回值。
    Trace.Warn("EndCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());

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

    try {
        // 结束异步调用,获取调用结果。如果有异常,也会在这里抛出。
        string result = http.EndSendHttpRequest(ar);
        labMessage.Text = string.Format("{0} => {1}", str, result);
    }
    catch( Exception ex ) {
        labMessage.Text = string.Format("{0} => Error: {1}", str, ex.Message);
    }
}

private void TimeoutCall(IAsyncResult ar)
{
    // 到这个方法,就表示任务执行超时了。
    Trace.Warn("TimeoutCall ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());

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

    labMessage.Text = string.Format("{0} => Timeout.", str);
}

前面我说过PageAsyncTask是支持超时的,那么它的超时功能是如何使用的呢,上面的示例只是给了一个超时的回调委托而已。

在开始演示PageAsyncTask的高级功能前,有必要说明一下示例所调用的服务端代码。 本示例所调用的服务是【C#客户端的异步操作】中使用的演示服务, 服务代码如下:

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

下面的示例我将演示开始二个异步任务,并设置异步页的超时时间为4秒钟。

protected void button1_click(object sender, EventArgs e)
{
    Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());

    // 设置页面超时时间为4秒
    Page.AsyncTimeout = new TimeSpan(0, 0, 4);

    // 注册第一个异步任务
    MyHttpClient<string, string> http = new MyHttpClient<string, string>();
    http.UserData = textbox1.Text;
    PageAsyncTask task = new PageAsyncTask(BeginCall, EndCall, TimeoutCall, http);
    RegisterAsyncTask(task);

    // 注册第二个异步任务
    MyHttpClient<string, string> http2 = new MyHttpClient<string, string>();
    http2.UserData = "T2_" + Guid.NewGuid().ToString();
    PageAsyncTask task2 = new PageAsyncTask(BeginCall2, EndCall2, TimeoutCall2, http2);
    RegisterAsyncTask(task2);
}

此页面的执行过程如下:

确实,第二个任务执行超时了。

再来看一下PageAsyncTask所支持的任务的并行执行是如何调用的:

protected void button1_click(object sender, EventArgs e)
{
    Trace.Write("button1_click ThreadId = " + System.Threading.Thread.CurrentThread.ManagedThreadId.ToString());

    // 设置页面超时时间为4秒
    Page.AsyncTimeout = new TimeSpan(0, 0, 4);

    // 注册第一个异步任务
    MyHttpClient<string, string> http = new MyHttpClient<string, string>();
    http.UserData = textbox1.Text;
    PageAsyncTask task = new PageAsyncTask(BeginCall, EndCall, TimeoutCall, http, true /*注意这个参数*/);
    RegisterAsyncTask(task);

    // 注册第二个异步任务
    MyHttpClient<string, string> http2 = new MyHttpClient<string, string>();
    http2.UserData = "T2_" + Guid.NewGuid().ToString();
    PageAsyncTask task2 = new PageAsyncTask(BeginCall2, EndCall2, TimeoutCall2, http2, true /*注意这个参数*/);
    RegisterAsyncTask(task2);
}

此页面的执行过程如下:

图片清楚地反映出,这二个任务是并行执行时,所以,这二个任务能在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个事件的异步任务全执行完了。


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
ASP.NETAtlas客户端库的概念发布时间:2022-07-10
下一篇:
ASP.NET中常用的26个优化性能方法发布时间:2022-07-10
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap