细说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
}
相关推荐
漂亮的摩托1 小时前
如何编写一个SpringBoot项目告警推送的Starter
java·spring boot·后端
任性的芝麻2 小时前
ASP.NET MVC 中的异步方式
后端·asp.net·mvc
雨师@2 小时前
go语言项目--实例化(图书管理)--006
开发语言·后端·golang
kuro-shiro2 小时前
SpringBoot 启动流程
java·spring boot·后端
独孤九剑打醒他11 小时前
双层Master-Worker软硬协同调度架构:从根源解决分布式数据一致性难题
后端·嵌入式硬件·硬件架构·硬件工程
周小码12 小时前
10分钟搭建管理后台:laravel-admin实战入门
php·laravel
不会c+12 小时前
02-SpringBoot配置文件
java·spring boot·后端
dog25012 小时前
从重尾到截断流量模型的演进
开发语言·php
雨辰AI14 小时前
生产级实战:人大金仓 V9 标准化运维手册(日常巡检 + 监控告警 + 应急处置)
java·运维·数据库·后端