超酷代码(ASP.NET 2.0 中的异步页面)

目录


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。

Back to top

ASP.NET 2.0 中的异步页面

ASP.NET 2.0 极大地简化了异步页面的构建过程。要开始构建异步页面,首先要在页面的 @ Page 指令中添加如下的 Async="true" 的属性:

<%@ 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 被调用,呈现被显示。并且由于 .NET Framework 2.0 版提供多种执行异步操作的途径,您甚至在多数情况下无需执行 IAsyncResult。Framework 会为您执行它。

图 1 中的代码隐藏类为我们提供了一个示例。相应的页面包含一个 Label 控件,其 ID 为“Output”。该页面使用 System.Net.HttpWebRequest 类来获取 http://msdn.microsoft.com 上的内容。随后它对返回的 HTML 进行分析并向 Label 控件中写入一个列表,其中列出了它所找到的所有 HREF 目标。

由于 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 同步页面处理与异步页面处理
图 2  同步页面处理与异步页面处理

图 2 说明了 ASP.NET 2.0 中同步页面与异步页面的不同之处。当同步页面被请求时,ASP.NET 会为该请求分配一个来自线程池的线程,并在此线程上执行该页面。如果该请求暂停并转而执行一项 I/O 操作,则此线程会被占用直至 I/O 操作完成。这样页面的整个生命周期才算完成。比较而言,异步页面是通过 PreRender 事件正常执行的。随后,使用 AddOnPreRenderCompleteAsync 注册的 Begin 方法将被调用,之后用于处理请求的线程会返回线程池。Begin 会启动一个异步 I/O 操作。操作完成后,ASP.NET 从线程池获取另一线程并调用 End 方法,在此线程上执行页面生命周期的剩余部分。

图 3 跟踪输出功能显示了异步页面的异步点
图 3  跟踪输出功能显示了异步页面的异步点

对 Begin 的调用就是页面的“异步点”。图 3 中的跟踪显示了异步点出现的准确位置。如果要调用 AddOnPreRenderCompleteAsync,则必须在异步点前调用,也就是说,其调用不得晚于页面的 PreRender 事件。

Back to top

异步数据绑定

对 ASP.NET 页面而言,直接使用 HttpWebRequest 来请求其他页面的现象并不常见,但对数据库的查询却是屡见不鲜,而且数据通常会与结果绑定。那么如何使用异步页面进行异步数据绑定呢?图 4 中的代码隐藏类为我们提供了一种实现绑定的方法。

AsyncDataBind.aspx.cs 使用的是 AsyncPage.aspx.cs 所使用的 AddOnPreRenderCompleteAsync 模式。但是,AsyncDataBind.aspx.cs 的 BeginAsyncOperation 方法并不调用 HttpWebRequest.BeginGetResponse,而是调用 ADO.NET 2.0 中新增的 SqlCommand.BeginExecuteReader 来执行异步数据库查询。调用完成后,EndAsyncOperation 会调用 SqlCommand.EndExecuteReader 来获取一个 SqlDataReader,后者随后被储存在一个私有字段中。PreRenderComplete 事件(在异步操作完成后与页面呈现之前的这段时间里触发)的事件处理程序中,SqlDataReader 被绑定至 Output GridView 控件。表面上看,这时的页面貌似一个很正常的同步页面,它使用 GridView 来呈现数据库查询结果。但是从内部看,该页面更加具有可伸缩性,因为它并未停留在一个等待查询返回的线程池线程上。

Back to top

异步调用 Web 服务

ASP.NET 网页经常执行的另一项与 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 的方法,则一个 .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) 的委托和传递给该处理程序的第二个参数 (FooCompletedEventArgs) 都是与 Web 服务代理一同生成的。您可以通过 FooCompletedEventArgs.Result 访问 Foo 的返回值。

图 6 所示的代码隐藏类使用 MethodAsync 模式对 Web 服务的 GetTitles 方法进行异步调用。从功能上讲,此页面与图 5 中的页面完全相同。但二者的内部构造却截然不同。AsyncWSInvoke2.aspx 包含一个类似于 AsyncWSInvoke1.aspx 的 @ Page Async="true" 指令。但 AsyncWSInvoke2.aspx.cs 并不调用 AddOnPreRenderCompleteAsync,而是为 GetTitlesCompleted 事件注册一个处理程序,并在 Web 服务代理上调用 GetTitlesAsync。ASP.NET 仍然会推迟呈现页面,直至 GetTitlesAsync 完成。究其本质,这里用到了 2.0 版本中另一个新增的类(即 System.Threading.SynchronizationContext)的一个实例,以接收异步调用开始和完成时的通知。

使用 MethodAsync 而不是 AddOnPreRenderCompleteAsync 来实现异步页面,有两个优点。其一,MethodAsync 能够向 MethodCompleted 事件处理程序传递模拟、区域性和 HttpContext.Current。而 AddOnPreRenderCompleteAsync 无法做到这一点。其二,如果页面进行多次异步调用并且必须在全部调用完成后才能得以呈现,那么要使用 AddOnPreRenderCompleteAsync 就必须编写一个 IAsyncResult,并且在全部调用完成前该 IAsyncResult 无法获得信号。而使用 MethodAsync 则无需如此大费周章。您只管进行调用,想调用多少就调用多少,ASP.NET 引擎会在最后一次调用返回后才呈现页面。

Back to top

异步任务

要在一个异步页面中进行多次 Web 服务异步调用,并要在全部调用完成后才呈现该页面,使用 MethodAsync 显得非常便捷。但如果您想在一个异步页面中执行多个异步 I/O 操作,并且不希望这些操作不涉及 Web 服务,该如何操作呢?这是否意味着需要重新编写一个 IAsyncResult 来返回到 ASP.NET,以便告诉 ASP.NET 最后一次调用于何时完成?幸运的是,不需要这么做。

在 ASP.NET 2.0 中,System.Web.UI.Page 类引入了另外一种能够便于异步操作的方法:这就是 RegisterAsyncTask。与 AddOnPreRenderCompleteAsync 相比,RegisterAsyncTask 具有四点优势。首先,除了 Begin 方法和 End 方法,RegisterAsyncTask 允许您注册一个超时方法。如果完成一个异步操作的时间过长,可以调用该超时方法。您可以在页面的 @ Page 指令中添加一个 AsyncTimeout 属性,以声明的方式设置超时。AsyncTimeout="5" 将超时设置为 5 秒。第二点优势,您可以在一个请求中多次调用 RegisterAsyncTask 来注册多个异步操作。当使用 MethodAsync 时,ASP.NET 会在全部操作完成后才呈现页面。第三,您可以使用 RegisterAsyncTask 的第四个参数将状态传递给 Begin 方法。最后一点优势,RegisterAsyncTask 能够将模拟、区域性和 HttpContext.Current 传递给 End 方法和 Timeout 方法。而如上文所述,使用 AddOnPreRenderCompleteAsync 注册的 End 方法则无法达到相同的效果。

就其他方面而言,依靠 RegisterAsyncTask 的异步页面与依靠 AddOnPreRenderCompleteAsync 的异步页面是相似的。它仍然需要在 @ Page 指令中插入一个 Async="true" 的属性(或者用编程方法将页面的 AsyncMode 属性设置为“true”,以达到同样的目的),仍然通过 PreRender 事件正常执行,此时会对使用 RegisterAsyncTask 注册的 Begin 方法进行调用,并且会在最后一次操作完成前一直对请求做进一步处理。可以看出,图 7 中与图 1 中的代码隐藏类在功能上是等同的,但是图 7 中的代码隐藏类使用的是 RegisterTaskAsync,而并非 AddOnPreRenderCompleteAsync。请注意,当完成 HttpWebRequest.BeginGetRequest 所用时间过长时,会调用名为 TimeOutAsyncOperation 的超时处理程序。相应的 .aspx 文件包含一个 AsyncTimeout 属性,该属性将超时间隔设置为 5 秒。还要注意的是,空值被传递给 RegisterAsyncTask 的第四个参数,该参数原本要用于向 Begin 方法传递数据。

RegisterAsyncTask 的主要优点是允许异步页面触发多次异步调用,并在所有调用完成后才呈现页面。它对于单个异步调用也表现极佳,并且它提供了一个 AddOnPreRenderCompleteAsync 所不具备的超时选项。如果您所创建的异步页面仅进行一个异步调用,可以使用 AddOnPreRenderCompleteAsync 或 RegisterAsyncTask。但是对于执行两次或者多次异步调用的异步页面,RegisterAsyncTask 能够极大地简化您的工作。

由于超时值是一种基于页面的设置,并非基于调用,您也许在想是否有可能改变单个调用的超时值。一句话,这是不可能的。您可以通过编程修改页面的 AsyncTimeout 属性,从而依次改变各项请求的超时值,但却无法为同一请求的不同调用设置不同的超时值。

Back to top

总结

现在您应该对 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 方法)通常是安全的,因为这些方法要使用完成端口来执行异步操作。

 
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值