ASP.NET 2.0 提供了多种新的功能,从声明性数据绑定和母版页到成员资格和角色管理服务,一应俱全。但是我认为最酷的新功能则是异步页面,下面就让我来告诉您原因。
当接收到一个页面请求时,ASP.NET 会从一个线程池中获取一个线程,并将页面请求分配给该线程。一个普通的,或者说是同步的页面在请求期间会占用线程,以防止线程被用于处理其他请求。如果同步请求变为 I/O 密集状态,例如,当该请求调用一个远程 Web 服务或查询远程数据库并等待调用返回时,则分配给它的线程在调用返回前会始终处于闲置状态。这种情况会限制可伸缩性,因为线程池中的可用线程是有限的。如果处理请求的所有线程都因等待 I/O 操作的完成而阻塞,则会有多余的请求排队等待这些线程的释放。最好的情况是出现吞吐量降低,因为需要等待更长的等待才能处理请求。最糟糕的情况是队列被填满而 ASP.NET 无法处理后续请求,并提示 503“服务器不可用”错误。
异步页面的出现为解决 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 本身并不支持异步页面,但是只要一点耐心和想象力就可以构建它们。要深入了解有关内容,请参阅 Fritz Onion 发表在 2003 年 6 月份的《MSDN杂志》上的文章“在您的服务器端 Web 代码中使用线程并构建异步处理程序”。
技巧就在于在页面的代码隐藏类中实现 IHttpAsyncHandler,使 ASP.NET 不再调用页面的 IHttpHandler.ProcessRequest 方法,而是通过调用 IHttpAsyncHandler.BeginProcessRequest 来处理各种请求。这样在您的 BeginProcessRequest 实现部分就可以启动另一个线程。该线程调用 base.ProcessRequest,使得页面在一个非线程池线程上对请求进行正常处理(诸如 Load 事件和 Render 事件等全部包括)。同时,BeginProcessRequest 在启动了新线程后立即返回,使得执行 BeginProcessRequest 的线程能够返回线程池。
以上只是基本原理,但是具体的细节却远不止这些。除此之外,您还需要执行 IAsyncResult 并在 BeginProcessRequest 将其返回。这显然意味着要创建一个 ManualResetEvent 对象,并当后台线程中返回 ProcessRequest 时向该对象发送信号。此外,您需要一个线程来调用 base.ProcessRequest。不幸的是,大多数能够将工作转移至后台线程的传统技术,包括 Thread.Start、ThreadPool.QueueUserWorkItem 和异步委托,都无法在 ASP.NET 应用程序中达到预期效果,因为它们要么会从线程池中窃取线程,要么有可能造成线程无限制地增长。正确实现异步页面需要使用自定义的线程池,而编写自定义线程池也不是件容易的事。(有关详细信息,请参阅 2005 年 2 月份的《MSDN 杂志》中的“.NET 相关问题”专栏)。
坦白讲,在 ASP.NET 1.x 中构建异步页面并非天方夜谭,但是要做到这一点非常麻烦。而且在尝试过这种滋味后,您会情不自禁地渴望一种更好的解决办法。现在我们有了解决方法,那就是 ASP.NET 2.0。
ASP.NET 2.0 中的异步页面
ASP.NET 2.0 极大地简化了异步页面的构建过程。要开始构建异步页面,首先要在页面的 @ Page 指令中添加如下的 Async="true" 的属性:
<%@ Page Async="true" ... %>
究其本质,这段代码的作用是告诉 ASP.NET 在页面中执行 IHttpAsyncHandler。接下来,您需要在页面生存期的早期(例如,在 Page_Load 期间)调用新的 Page.AddOnPreRenderCompleteAs
AddOnPreRenderCompleteAsync ( new BeginEventHandler(MyBeginMethod), new EndEventHandler (MyEndMethod) );
接下来是精彩的部分。页面继续进行正常的处理过程,直至稍后触发 PreRender 事件。ASP.NET 会调用先前使用 AddOnPreRenderCompleteAs
图
由于 HTTP 请求需要很长时间才能返回,AsyncPage.aspx.cs 会异步执行处理。它会在 Page_Load 中注册 Begin 方法和 End 方法,并在 Begin 方法中调用 HttpWebRequest.BeginGetResponse 来启动一个异步 HTTP 请求。BeginAsyncOperation 将 BeginGetResponse 返回的 IAsyncResult 返回至 Asp.NET,从而 ASP.NET 能够在 HTTP 请求完成时调用 EndAsyncOperation。接着,EndAsyncOperation 将对内容进行分析,并将结果写至 Label 控件,随后进行呈现,并向浏览器返回一个 HTTP 响应。

图 2
图

图 3
对 Begin 的调用就是页面的“异步点”。图
异步数据绑定
对 ASP.NET 页面而言,直接使用 HttpWebRequest 来请求其他页面的现象并不常见,但对数据库的查询却是屡见不鲜,而且数据通常会与结果绑定。那么如何使用异步页面进行异步数据绑定呢?图
AsyncDataBind.aspx.cs 使用的是 AsyncPage.aspx.cs 所使用的 AddOnPreRenderCompleteAs
异步调用 Web 服务
ASP.NET 网页经常执行的另一项与 I/O 有关的任务是调用 Web 服务。由于 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 的方法,则一个 .NET Framework 2.0 版的 Web 服务代理除了具有名为 Foo、BeginFoo 和 EndFoo 的方法外,还包含一个名为 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
图
使用 MethodAsync 而不是 AddOnPreRenderCompleteAs
异步任务
要在一个异步页面中进行多次 Web 服务异步调用,并要在全部调用完成后才呈现该页面,使用 MethodAsync 显得非常便捷。但如果您想在一个异步页面中执行多个异步 I/O 操作,并且不希望这些操作不涉及 Web 服务,该如何操作呢?这是否意味着需要重新编写一个 IAsyncResult 来返回到 ASP.NET,以便告诉 ASP.NET 最后一次调用于何时完成?幸运的是,不需要这么做。
在 ASP.NET 2.0 中,System.Web.UI.Page 类引入了另外一种能够便于异步操作的方法:这就是 RegisterAsyncTask。与 AddOnPreRenderCompleteAs
就其他方面而言,依靠 RegisterAsyncTask 的异步页面与依靠 AddOnPreRenderCompleteAs
RegisterAsyncTask 的主要优点是允许异步页面触发多次异步调用,并在所有调用完成后才呈现页面。它对于单个异步调用也表现极佳,并且它提供了一个 AddOnPreRenderCompleteAs
由于超时值是一种基于页面的设置,并非基于调用,您也许在想是否有可能改变单个调用的超时值。一句话,这是不可能的。您可以通过编程修改页面的 AsyncTimeout 属性,从而依次改变各项请求的超时值,但却无法为同一请求的不同调用设置不同的超时值。
总结
现在您应该对 ASP.NET 2.0 中提供的异步页面有一个深入的了解。在即将推出的新版 ASP.NET 中,实现异步页面将变得更加便捷,其架构允许您在一个请求中批量执行多次异步 I/O 操作,并且您可以在所有操作完成后再呈现页面。异步 ASP.NET 页面结合异步 ADO.NET 和 .NET Framework 中的其他异步功能,提供了一种强大而便捷的方案来解决 I/O 密集型请求由于线程池拥挤而导致可伸缩性受限的问题。
在构建异步页面时您需要记住最后一点,即不要从 ASP.NET 使用的线程池中借用线程来启动异步操作。例如,在页面的异步点调用 ThreadPool.QueueUserWorkItem 会适得其反,因为此方法会借用线程池中的线程,导致没有线程可用于处理请求。比较而言,调用 Framework 中内建的方法(如 HttpWebRequest.BeginGetResponse 方法和 SqlCommand.BeginExecuteReader 方法)通常是安全的,因为这些方法要使用完成端口来执行异步操作。