在上篇【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的实现,它是异步模式的核心。