下载本文的代码: WickedCode0510.exe (123KB)
本页内容
ASP.NET 1.x 中的异步页 | |
ASP.NET 2.0 中的异步页 | |
异步数据绑定 | |
异步调用 Web 服务 | |
异步任务 | |
包装它 |
ASP.NET 2.0 提供了大量新功能,其中包括声明性数据绑定和母版页,成员和角色管理服务等。但我认为最棒的功能是异步页,接下来让我告诉您其中的原因。
当 ASP.NET 接收针对页的请求时,它从线程池中提取一个线程并将请求分配给该线程。一个普通的(或同步的)页在该请求期间保留线程,从而防止该线程用于处理其他请求。如果一个同步请求成为 I/O 绑定(例如,如果它调用一个远程 Web 服务或查询一个远程数据库,并等待调用返回),那么分配给该请求的线程在调用返回之前处于挂起状态。这影响了可伸缩性,原因是线程池的可用线程是有限的。如果所有请求处理线程全部阻塞以等待 I/O 操作完成,则其他请求排入队列等待线程释放。最好的情况是吞吐量减少,因为请求等待较长的时间才能得到处理。最坏的情况则是该队列填满,并且 ASP.NET 因 503“Server Unavailable”错误使后续请求失败。
异步页为由 I/O 绑定的请求引起的问题提供优秀的解决方案。页处理从线程池线程开始,但是当一个异步 I/O 操作开始响应 ASP.NET 的信号之后,该线程返回线程池。当该操作完成时,ASP.NET 从线程池提取另一个线程,并完成该请求的处理。由于线程池线程得到了更高效的使用,因此提高了可伸缩性。那些挂起等待 I/O 完成的线程现在可用于服务其他请求。直接的受益方是不执行长时间 I/O 操作并因此可以快速进出管线的请求。长时间等待进入管线会对此类请求的性能带来不小的负面影响。
ASP.NET 2.0 Beta 2 异步页基础结构的相关文档很少。让我们展望一下异步页的前景,从而弥补这点不足。请记住,本专栏涉及 ASP.NET 2.0 和 .NET Framework 2.0 的测试版本。
ASP.NET 1.x 中的异步页
ASP.NET 1.x 本质上不支持异步页,但是通过坚韧的努力和不懈地创新可以生成异步页。有关更多概述信息,请参阅 MSDN®Magazine 2003 年 6 月刊的文章“Use Threads and Build Asynchronous Handlers in Your Server-Side Web Code”,该文章的作者是 Fritz Onion。
这里的技巧是,在一个页的代码隐藏类中实现 IhttpAsyncHandler,从而提示 ASP.NET 通过调用 IHttpAsyncHandler.BeginProcessRequest 来处理请求,而不是通过调用该页的 IHttpHandler.ProcessRequest 方法。然后,您的 BeginProcessRequest 实现可以启动另一个线程。该线程调用 base.ProcessRequest,使得页进入其常规请求处理生命周期(完成诸如 Load 和 Render 的事件),但是在非 ThreadPool 线程上例外。同时,启动新线程之后 BeginProcessRequest 立即返回,从而允许执行 BeginProcessRequest 的线程返回线程池。
这是基本思想,但细节中还有很多注意事项。其中,您需要实现 IAsyncResult,并从 BeginProcessRequest 中返回它。这通常意味着创建一个 ManualResetEvent 对象,并且当 ProcessRequest 在后台线程中返回时向其发送信号。此外,您必须提供调用 base.ProcessRequest 的线程。遗憾的是,多数用于将工作移到后台线程的常规技术(包括 Thread.Start、ThreadPool.QueueUserWorkItem 和异步委托)在 ASP.NET 应用程序中都是起反作用的,因为它们或者从线程池“偷盗”线程,或者有不受限制的线程增长的危险。正确的异步页实现使用自定义线程池,但自定义线程池类不容易编写(有关更多信息,请参阅 MSDN Magazine 2005 年 2 月刊的 .NET Matters 专栏)。
主要是在 ASP.NET 1.x 中生成异步页并非不可能,而是有些乏味。在尝试一、两次之后,您不禁会想一定会有更好的方法。目前,这个好方法就是 ASP.NET 2.0。
ASP.NET 2.0 中的异步页
ASP.NET 2.0 极大地简化了生成异步页的方式。首先使用该页的 @ Page 指令引入 Async=“true” 属性,如下所示:
在后台,这会通知 ASP.NET 在该页中实现 IhttpAsyncHandler。接下来,您在该页生存期的早期(例如,在 Page_Load 时)调用新的 Page.AddOnPreRenderCompleteAsync 方法来注册一个 Begin 方法和一个 End 方法,如以下代码所示:
AddOnPreRenderCompleteAsync ( new BeginEventHandler(MyBeginMethod), new EndEventHandler (MyEndMethod) );
接下来的操作比较有趣。该页经历其常规处理生命周期,直到 PreRender 事件刚刚引发之后。然后,ASP.NET 调用使用 AddOnPreRenderCompleteAsync 注册的 Begin 方法。Begin 方法的任务是启动诸如数据库查询或 Web 服务调用的异步操作,并立即返回。此时,分配给该请求的线程返回到线程池。此外,Begin 方法返回 IAsyncResult,它允许 ASP.NET 确定异步操作完成的时间,这个时候 ASP.NET 从线程池提取线程并调用 End 方法。当 End 返回之后,ASP.NET 执行该页生命周期其余的部分,包括呈现阶段。在 Begin 返回以及调用 End 之间,该请求处理线程可以自由地服务于其他请求,直至调用 End 且延迟呈现为止。由于 2.0 版的 .NET Framework 提供多种执行异步操作的方式,因此,您甚至无需实现 IasyncResult。反之,Framework 替您实现。
图 1 中的代码隐藏类提供一个示例。响应页包含一个 ID 为“Output”的 Label 控件。该页使用 System.Net.HttpWebRequest 类提取 http://MSDN.microsoft.com 的内容。然后,它分析返回的 HTML,并将它发现的全部 HREF 目标列表写出到 Label 控件。
由于 HTTP 请求需要较长时间才能返回,因此,AsyncPage.aspx.cs 异步执行对它的处理。它在 Page_Load 中注册 Begin 和 End 方法,并且在 Begin 方法中,它调用 HttpWebRequest.BeginGetResponse 启用一个异步 HTTP 请求。BeginAsyncOperation 将由 BeginGetResponse 返回的 IAsyncResult 返回到 ASP.NET,导致当 HTTP 请求完成时,ASP.NET 调用 EndAsyncOperation。EndAsyncOperation 进而分析该内容并将结果写入 Label 控件,之后进行呈现,并且 HTTP 响应返回到浏览器。
图 2 同步和异步页处理
图 2 说明 ASP.NET 2.0 同步和异步页之间的区别。当请求同步页时,ASP.NET 为该请求分配线程池中的一个线程,并在该线程上执行页。如果该请求停止执行 I/O 操作,则挂起线程,直到完成操作,从而可以完成该页的生命周期。相反,异步页通常通过 PreRender 事件执行。然后,调用使用 AddOnPreRenderCompleteAsync 注册的 Begin 方法,之后,该请求处理线程返回线程池。Begin 启动一个异步 I/O 操作,当该操作完成时,ASP.NET 从线程池提取另一个线程并调用 End 方法,并且在该线程上执行该页生命周期的其余部分。
图 3 跟踪输出显示异步页的异步点
对 Begin 的调用标记该页的“异步点”。图 3 中的跟踪准确显示异步点发生在何处。如果调用,则必须在异步点之前调用 AddOnPreRenderCompleteAsync — 即,不晚于该页的 PreRender 事件。
异步数据绑定
通常情况下,ASP.NET 页并不使用 HttpWebRequest 直接请求其他页,但它们通常查询数据库并对结果进行数据绑定。因此,您将如何使用异步页执行异步数据绑定呢?图 4 中的代码隐藏类显示进行此操作的一种方式。
AsyncDataBind.aspx.cs 与 AsyncPage.aspx.cs 使用相同的 AddOnPreRenderCompleteAsync 模式。但是,AsyncDataBind.aspx.cs 的 BeginAsyncOperation 方法调用 ADO.NET 2.0 中的新方法 SqlCommand.BeginExecuteReader(而非 HttpWebRequest.BeginGetResponse),以执行一个异步数据库查询。当调用完成时,EndAsyncOperation 调用 SqlCommand.EndExecuteReader 以获取 SqlDataReader,然后将其存储在私有字段中。在用于 PreRenderComplete 事件(在异步操作完成但呈现该页之前引发)的事件处理程序中,AsyncDataBind.aspx.cs 之后将 SqlDataReader 绑定到 Output GridView 控件。从外观上看,该页类似于使用 GridView 呈现数据库查询结果的普通(同步)页。但是在内部,该页更具可伸缩性,因为它并不挂起线程池线程以等待查询返回。
异步调用 Web 服务
另一个通常由 ASP.NET Web 页执行的、与 I/O 相关的任务是调出 Web 服务。由于 Web 服务调用花费较长时间才能返回,因此,执行它们的页是用于异步处理的理想选择。
图 5 显示生成调出 Web 服务的异步页的方式。它使用图 1 和 图 4 中相同的 AddOnPreRenderCompleteAsync 机制。该页的 Begin 方法通过调用 Web 服务代理的异步 Begin 方法启动一个异步 Web 服务调用。该页的 End 方法在私有字段中缓存对 Web 方法返回的 DataSet 的引用,并且 PreRenderComplete 处理程序将 DataSet 绑定到 GridView。作为参考,该调用的目标 Web 方法如以下代码所示:
[WebMethod] public DataSet GetTitles () { string connect = WebConfigurationManager.ConnectionStrings ["PubsConnectionString"].ConnectionString; SqlDataAdapter adapter = new SqlDataAdapter ("SELECT title_id, title, price FROM titles", connect); DataSet ds = new DataSet(); adapter.Fill(ds); return ds; }
这只是其中一种方式,但并不是唯一的方式。.NET Framework 2.0 Web 服务代理支持两种对 Web 服务进行异步调用的机制。一个是 .NET Framework 1.x 和 2.0 Web 服务代理中的每方法 Begin 和 End 方法。另一个是仅由 .NET Framework 2.0 的 Web 服务代理提供的新 MethodAsync 方法和 MethodCompleted 事件。
如果一个 Web 服务有一个名为 Foo 的方法,那么除了具有名为 Foo、BeginFoo 和 EndFoo 的方法外,.NET Framework 版本 2.0 Web 服务代理还包括名为 FooAsync 的方法和名为 FooCompleted 的事件。可以通过注册 FooCompleted 事件的处理程序并调用 FooAsync 来异步调用 Foo,如下所示:
proxy.FooCompleted += new FooCompletedEventHandler (OnFooCompleted); proxy.FooAsync (...); ... void OnFooCompleted (Object source, FooCompletedEventArgs e) { // Called when Foo completes }
当异步调用由于 FooAsync 完成而开始时,将引发 FooCompleted 事件,从而导致调用 FooCompleted 事件处理程序。包装该事件处理程序 (FooCompletedEventHandler) 的委托和传递给它的第二个参数 (FooCompletedEventArgs) 都随 Web 服务代理一起生成。可通过 FooCompletedEventArgs.Result 访问 Foo 的返回值。
图 6 展示使用 MethodAsync 模式异步调用 Web 服务的 GetTitles 方法的代码隐藏类。从功能上讲,该页等同于图 5 中的页。但其内部实现则大为不同。AsyncWSInvoke2.aspx 包括一个 @ Page Async=“true” 指令,类似于 AsyncWSInvoke1.aspx。但是,AsyncWSInvoke2.aspx.cs 并不调用 AddOnPreRenderCompleteAsync;它注册一个用于 GetTitlesCompleted 事件的处理程序,并调用 Web 服务代理上的 GetTitlesAsync。ASP.NET 仍然延迟呈现该页,直到 GetTitlesAsync 完成。在内部,当异步调用开始以及完成时,它使用 System.Threading.SynchronizationContext 的一个实例(2.0 的一个新类)接收通知。
使用 MethodAsync 而非 AddOnPreRenderCompleteAsync 实现异步页有两个优势。首先,MethodAsync 将模拟、区域性和 HttpContext.Current 注入 MethodCompleted 事件处理程序,而 AddOnPreRenderCompleteAsync 则不然。其次,如果该页进行多个异步调用,而且必须延迟呈现直到所有调用完成,则使用 AddOnPreRenderCompleteAsync 要求您生成一个在所有调用完成前保持无信号状态的 IasyncResult。使用 MethodAsync,这样的操作就不是必需的;您只需放置这些调用(数量不限),ASP.NET 引擎延迟该呈现阶段,直到最后一个调用返回。
异步任务
MethodAsync 是从异步页进行多个异步 Web 服务调用并延迟呈现阶段直到所有调用完成的一个简便方法。但如果您想在一个异步页中执行若干异步 I/O 操作,而且这些操作不涉及 Web 服务,那该如何呢? 这么说,可以反过来生成一个 IAsyncResult,它可以返回到 ASP.NET 以允许它了解最后一个调用何时完成的吗? 幸运的是,答案是否定的。
在 ASP.NET 2.0 中,System.Web.UI.Page 类引入了另一个方法来简化异步操作: RegisterAsyncTask。RegisterAsyncTask 比 AddOnPreRenderCompleteAsync 具有四个优势。首先,除了 Begin 和 End 方法,RegisterAsyncTask 还允许您注册当异步操作长时间无法完成时调用的超时方法。您可以通过在该页的 @ Page 指令中包含 AsyncTimeout 属性以声明性方式设置超时。AsyncTimeout="5" 将超时设置为 5 秒。第二个优势是,您可以在一个请求中多次调用 RegisterAsyncTask 来注册若干异步操作。和使用 MethodAsync 一样,ASP.NET 延迟呈现该页,直到所有操作完成。第三,您可以使用 RegisterAsyncTask 的第四个参数将状态传递给 Begin 方法。最后,RegisterAsyncTask 将模拟、区域性和 HttpContext.Current 注入 End 和 Timeout 方法。正如本文前面提到的,使用 AddOnPreRenderCompleteAsync 注册的 End 方法的情况则不然。
在其他方面,依赖于 RegisterAsyncTask 的异步页与依赖于 AddOnPreRenderCompleteAsync 的异步页相类似。它仍然需要 @ Page 指令(或等效的编程指令,它会将该页的 AsyncMode 属性设置为 true)中的 Async=“true” 属性,而且它仍然与平时一样通过 PreRender 事件执行,此时调用使用 RegisterAsyncTask 注册的 Begin 方法,而且进一步保持请求处理直到最后一个操作完成。例如,图 7 中的代码隐藏类在功能上与图 1 中的等效,但是它使用 RegisterTaskAsync 而非使用 AddOnPreRenderCompleteAsync。请注意名为 TimeoutAsyncOperation 的超时处理程序,如果 HttpWebRequest.BeginGetRequest 长时间无法完成,将调用该处理程序。相应的 .aspx 文件包括一个将超时间隔设置为 5 秒的 AsyncTimeout 属性。还请注意传给 RegisterAsyncTask 的第四个参数(可用于将数据传送到 Begin 方法)的 null。
RegisterAsyncTask 的主要优势在于,它允许异步页引发多个异步调用,并延迟呈现直到所有调用完成。它也很好地适用于单个异步调用,而且它提供了 AddOnPreRenderCompleteAsync 不具有的超时选项。如果生成一个只进行一个异步调用的异步页,您可以使用 AddOnPreRenderCompleteAsync 或 RegisterAsyncTask。但对于放置两个以上异步调用的异步页,RegisterAsyncTask 极大地简化了您的操作。
由于超时值是每页而非每调用设置,因此您可能想知道是否能改变单个调用的超时值。简单的回答是否。您可以通过以编程方式修改页的 AsyncTimeout 属性,逐个请求地更改超时,但是您无法将不同超时分配给从同一请求初始化的不同调用。
包装它
现在,您已经了解了 ASP.NET 2.0 中异步页的实质。它们在即将推出的 ASP.NET 版本中非常易于实现,并且其体系结构允许您在一个请求中批处理多个异步 I/O 操作,并延迟该页的呈现直到所有操作完成。通过与异步 ADO.NET 和 .NET Framework 中的其他新异步功能相结合,异步 ASP.NET 页针对因充满线程池而限制可伸缩性的 I/O 绑定请求问题提供了解决方案。
当生成异步页时最后需要注意的一点是,不应该启动来自 ASP.NET 使用的同一线程池的异步操作。例如,在页的异步点调用 ThreadPool.QueueUserWorkItem 会起反作用,因为该方法来自线程池,从而导致纯粹获取用于处理请求的零线程。相反,调用内置于 Framework 中的异步方法(例如,方法 HttpWebRequest.BeginGetResponse 和 SqlCommand.BeginExecuteReader)通常认为是安全的,因为这些方法倾向于使用完成端口实现异步行为