在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
在上篇博客【C#客户端的异步操作】,
我介绍了一些.net中实现异步操作的方法,在那篇博客中,我是站在整个.net平台的角度来讲述各种异步操作的实现方式,
并针对各种异步操作以及不同的编程模型给出了一些参考建议。上篇博客谈到的内容可以算是异步操作的基础,
今天我再来谈异步,专门来谈在ASP.NET平台下的各种异步操作。在这篇博客中,我主要演示在ASP.NET中如何使用各种异步操作。 由于本文是【C#客户端的异步操作】的续集, 因此一些关于异步的基础内容,就不再过多解释了。如不理解本文的示例代码,请先看完那篇博文吧。 在【C#客户端的异步操作】的结尾,
有一个小节【在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平台上最主要且默认的开发框架,我自然也会全面地介绍它所支持的各种异步方式。 该选哪个先出场呢?我想了很久,最后还是决定先请出处理请求的核心对象: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设计方式差不多。如果这样想,那么后面的事件就好理解了。 下面我们来看一下如何创建一个支持异步的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); } } 实现其实是比较简单的,大致可以总结如下: 这里要说明一下,在【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页的处理过程。 对于同步页来说,整个过程从头到尾连续执行一遍就行了,这比较容易理解。但是对于异步页来说,它必须要拆分成二个阶段, 以下图片反映了异步页的页面生命周期。注意右边的流程是代表异步页的。 这个图片是我从网上找的。原图比较小,字体较模糊,我将原图放大后又做了一番处理。本想在图片中再加点说明, 考虑到尊重原图作者,没有在图片上加上任何多余字符。下面我还是用文字来补充说明一下吧。 在上面的左侧部分是一个同步页的处理过程,右侧为一个异步页的处理过程。 引入这个图片只是为了能让您对于异步页的执行过程有个大致的印象: 它将原来一个线程连续执行的过程分成以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()就可以了。 下面来看一下我加上调试信息的页面执行过程吧。 从这张图片中,我们至少可以看到二个信息: 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, |
请发表评论