Fritz Onion

下载本文的代码:Onion2006_07.exe (176KB)

 
本页内容
  Web 部件拥塞
  异步 Web 访问
  工作原理
  异步数据访问

使用 ASP.NET 2.0 的门户基础结构,可以轻松构建包括可插入 Web 部件集合在内的自定义网站。此模型具有很高的灵活性,能够让用户轻松地将 Web 部件放到网页上的任何位置,因此可以自由地自定义网站。然而这些优势也会导致影响用户体验的低效行为,因为您无法预测将同时使用哪些组件,进而不能为每个单独的组件提供特定的数据检索优化。

一个典型门户网站中最为常见的低效行为发生于多个 Web 部件同时发出网络数据请求之时。每个请求,无论是请求 Web 服务或远程数据库,最终都会增加处理页面所需的总时间,即使这些请求之间彼此独立并且确保为并行发出。

幸运的是,ASP.NET 2.0 还引入了一款易于使用的异步页面模型,在与异步 Web 服务调用和异步数据库访问结合使用时,可以显著提高多个独立 Web 部件并行收集数据时的门户页面响应速度。本文中,我们将主要关注构建执行异步数据检索的 Web 部件,以使包含这些部件的门户页面具有更高的响应速度和可伸缩性的各项技术。

Web 部件拥塞

我们首先来看一下图 1 中显示的门户页面。在该示例中,门户页面中有四个 Web 部件,各自通过不同的来源检索数据。该示例应用的完整源代码可通过 MSDN®Magazine 网站下载,建议您在阅读本专栏之后查看该应用。在该示例中,有三个 Web 部件通过 Web 服务来检索数据,在返回之前有意等待三秒钟的时间。第四个 Web 部件会将一个 ADO.NET 查询发送至 SQL Server 数据库,同样在返回之前等待三秒钟时间。这个示例有些夸张,但也不是不可能发生。


图 1 门户页面示例

 

示例应用中的每个 Web 部件由用户控件构建而成,并将数据检索结果绑定到显示它的控件。每一控件的代码和标记数量保持在最低限度,因此该示例简单易懂,能让您更多地关注如何使 Web 部件实现异步。

以下是 NewsWebPart.ascx 用户控件文件:

   <%@ Control Language="C#"
AutoEventWireup="true"
CodeFile="NewsWebPart.ascx.cs"
Inherits="webparts_
NewsWebPart" %>
<asp:BulletedList ID="_newsHeadlines"
runat="server">
</asp:BulletedList>

如下是新闻标题示例 Web 部件对应的源代码:

public partial class webparts_NewsWebPart :UserControl
{
protected void Page_Load(object sender, EventArgs e)
{
PortalServices ps = new PortalServices();
_newsHeadlines.DataSource = ps.GetNewsHeadlines();
_newsHeadlines.DataBind();
}
}

请注意它与 Web 服务交互,以检索示例新闻标题的方式。股票报价 Web 部件和天气预报 Web 部件的执行过程大体相同,只不过是使用同一 Web 服务的不同方法来检索各自所需的数据。与此类似,图 2 显示的是销售报告示例 Web 部件的 SalesReportWebPart.ascx 用户控件文件及其内含代码页。请注意该控件如何使用 ADO.NET 从数据库中检索销售数据,并使用该数据来填充 GridView 控件。


图 3 Web 部件顺序处理

 

一旦示例门户页面开始运行,问题就会显现。请求的处理时间超过了 12 秒,这一延迟将会导致大多数用户不愿使用该应用。这一漫长延迟的原因如图 3 所示,其中跟踪了执行该页面时请求的执行路径。与页面控件层次结构中的其他控件一样,每一 Web 部件都是按照页面控件层次结构定义的顺序依次加载。由于该过程是按顺序进行,因此每一 Web 部件都必须等待层次结构中排在其前面的部件完成之后,才会开始其数据请求并准备响应。再加上每一数据检索中人为的 3 秒延迟,就能明白为什么需要 12 秒才能完成响应了。每一 Web 部件相继执行完全独立的数据检索。此处需要注意的最为重要的一点是,这些检索操作能够并行执行,进而节省 75% 的响应时间。这就是我想达成的目标。

异步 Web 访问

在该示例中,三个 Web 部件使用 Web 服务来检索数据,一个部件使用 ADO.NET 来访问数据库。让我们从 Web 服务异步调用开始,这是因为由 Web 服务描述语言工具 WSDL.exe(或者 Visual Studio 2005 Add Web Service Reference 工具)生成的 Web 服务代理类对执行 Web 方法异步调用提供良好的支持。

在 ASP.NET 2.0 中创建 Web 服务代理类时,实际上会生成三种不同的调用任意特定方法的方式:一种同步,两种异步。例如,Web 部件所使用的 Web 服务代理可以通过如下方法来调用 GetNewsHeadlines Web 方法:

   public string[] GetNewsHeadlines()
public IAsyncResult BeginGetNewsHeadlines(
AsyncCallback callback, object asyncState)
public string[] EndGetNewsHeadlines(       IAsyncResult asyncResult)
public void GetNewsHeadlinesAsync()
public void GetNewsHeadlinesAsync(       object userState)
public event
GetNewsHeadlinesCompletedEventHandler
GetNewsHeadlinesCompleted;

第一个方法 GetNewsHeadlines 是标准的同步方法。后两个方法,BeginGetNewsHeadlines 和 EndGetNewsHeadlines 能够用于异步调用方法,并可通过标准的 IAsyncResult 接口来嵌套到任意数量的 .NET 异步机制中。

此方案中最值得关注的是最后一个方法:GetNewsHeadlinesAsync。为使用该特定方法,必须注册代理类事件委托,此为专门生成用于捕获异步调用结果(例如示例中的 GetNewsHeadlinesCompleted 事件)。委托签名为包含有方法返回值的强类型,因而可以从方法实现中轻松提取结果。

使用此基于事件的异步方法,可以轻松地重写标题新闻 Web 部件中需要异步的 Web 方法调用,如图 4 所示。首先订阅代理类 GetNewsHeadlinesCompleted 事件的委托,然后调用 GetNewsHeadlinesAsync 方法。在订阅完成事件方法的实现过程中,将 Web 方法调用结果与 BulletedList 绑定起来显示给客户。另外需要注意的一点是,只有将 Web 部件放置在具有 Async="true" 属性设置的页面之中,这些异步方法才会起作用。可通过查看包含页面中的 IsAsync 属性,以编程方式进行检查。如果放置 Web 部件的页面并非异步,则需要求助于标准的同步绑定,如图 4 所示。

现在,为保证异步 Web 部件可以执行其异步数据检索,必须将其放在 Async 属性设置为 true 的页面上,因此,请按照如下方式来修改门户页面的 Page 指令:

<%@ Page Language="C#" AutoEventWireup="true"  Async="true" %>

另外两个使用 Web 服务进行异步数据检索的 Web 部件完成更新之后,门户页面的响应速度会更快。实际上,根据不同的部件加载顺序,客户可在 3 秒左右得到响应结果(如果首先加载销售 Web 部件则需 6 秒左右)!即使销售报告 Web 部件仍在顺序访问数据库,其他三个 Web 部件也会同时执行他们 Web 服务异步调用,因此主请求线程不会等候太长时间。当然了,其最终目的是为了使所有的 I/O 绑定实现异步工作,如此一来,客户可以同时使用 Web 服务和数据库驱动的 Web 部件,从而避免了不必要的顺序阻塞问题。

推动 I/O 绑定异步处理 I/O 请求的另一个原因是,将主线程释放回线程池以处理其他请求。我目前的做法是仅在完成销售报告数据库查询之后才重新释放线程,也就是说要占用原本可以处理其他请求的线程池线程整整 3 秒钟。如果把最后一个 I/O 绑定数据请求也设为异步,则页面使用请求线程的时间将会仅为释放所有异步 I/O 请求的时间,随后立即返回线程池。

工作原理

如果您曾经从事过异步编程工作,您可能会感觉到针对 Web 服务调用的局部更改还远远不够。我还没涉及到 IAsyncResult 接口,也无需让包含页面获悉正在执行异步操作(通过注册任务或其他技术),但一切就可以按照我希望的方式工作。

秘密就在于异步方法 Web 服务代理类的实现,以及 Microsoft® .NET Framework 2.0 中引入的、名为 AsyncOperationManager 的帮助程序类。每当调用代理类 GetNewsHeadlinesAsync 方法时,会将调用映射至 SoapHttpClientProtocol 基类的内部帮助程序方法,称之为 InvokeAsync,代理类即由此派生而出。InvokeAsync 有两个重要作用:通过调用 AsyncOperationManager 静态 CreateOperation 方法来注册异步操作以及使用 WebRequest 类的 BeginGetRequestStream 方法来启动异步请求。此时调用返回,页面继续处理其生命周期,但由于页面已被标记为 Async="true" 属性,它将继续处理请求直至 PreRender 事件,然后将请求线程返回至线程池。异步 Web 请求完成之后,会通过 I/O 线程池中的一个单独线程,调用订阅到代理已完成事件的方法。如果这是最后需要完成的异步操作(基于 AsyncOperationManager 同步环境进行跟踪),将会回调页面,并且请求会从中断的位置继续完成其处理过程(从 PreRenderComplete 事件开始)。图 5 显示了在异步页面中使用异步 Web 请求这一完整的生命周期。


图 5 异步页面中的异步 Web 请求

 

AsyncOperationManager 是专门设计用来在不同环境下帮助异步方法调用管理的类。举例来说,如果从 Windows® Forms 应用中异步调用 Web 服务,会同时连接 AsyncOperationManager 类。每一环境之间的区别就是与 AsyncOperationManager 相关联的 SynchronizationContext。在基于 ASP.NET 的应用环境下运行时,SynchronizationContext 将被设置为 AspNetSynchronizationContext 类的一个实例。其主要目的是跟踪等待处理的异步请求数量,以便在这些请求处理完毕后,可以继续页面请求处理。对比之下,在基于 Windows Forms 的应用环境下运行时,SynchronizationContext 将被设置为 WindowsFormsSynchronizationContext 类的一个实例。其主要目的是使从后台线程到 UI 线程间的调用封送处理更为简单。

异步数据访问

现在,让我们回到最后的 Web 部件异步问题和使用 ADO.NET 执行异步数据检索的常见问题。遗憾的是,没有类似于 Web 服务代理所提供的简单异步机制来执行异步数据检索,所以就不得不做一些额外工作,以使最终 Web 部件真正实现异步。我可以配合使用 SqlCommand 类的新异步方法与 ASP.NET 的异步任务功能。通过 SqlCommand,可以使用下列方法之一来异步调用命令:

IAsyncResult BeginExecuteReader(AsyncCallback ac, object state)

IAsyncResult BeginExecuteNonQuery(AsyncCallback ac, object state)

IAsyncResult BeginExecuteXmlReader(AsyncCallback ac, object state)

数据流准备开始读取之后,就能够调用相应的完成方法:

SqlDataReader EndExecuteReader(IAsyncResult ar)

int EndExecuteNonQuery(IAsyncResult ar)

XmlReader EndExecuteXmlReader(IAsyncResult ar)

要想使用上述这些异步检索方法,必须在连接字符串中添加 "async=true"。这种情况下,我想通过将 GridView 绑定到 SqlDataReader 后再行填充,因此会使用 BeginExecuteReader 方法来启动异步调用。

为将此连接至异步页面,ASP.NET 2.0 允许注册需要在页面完成显示之前执行的异步任务。相比在 Web 服务代理中采用的模型而言,这是一个更为显式的模型,但也具有更大的灵活性。为注册异步任务,我创建了 PageAsyncTask 类的一个实例,并使用三个委托对其进行初始化:开始处理程序、结束处理程序和超时处理程序。开始处理程序必须返回一个 IAsyncResult 接口,以使用 BeginExecuteReader 由此启动异步数据请求。任务完成后调用结束处理程序(在本示例为准备读取数据时),此时可以使用这些结果。ASP.NET 会在开始释放请求线程之前调用开始处理程序(紧随 PreRender 事件完成之后)。图 6 显示了销售报告 Web 部件的更新实现,其使用异步任务和 SqlCommand 类的异步 BeginExecuteReader 方法来执行异步数据访问。

请注意,通过使用代理类提供的其他异步方法(例如 BeginGetNewsHeadlines),可在 Web 服务请求中使用这一相同技术。该技术的另一个潜在优势就是还可以指定一个超时处理程序。如果远程调用不能及时返回,将会调用关联的超时处理程序。请在 Page 指令中使用 AsyncTimeout 属性指定超时,默认值为 20 秒。还请注意,与使用基于事件的异步模式不同,使用 Page.RegisterAsyncTask 时,不需要根据 Page.IsAsync 结果来建立到异步调用的分支。 Web 部件的异步页面任务可以在异步页面中正常运行,甚至还能支持 Web 部件的并行执行。核心区别在于异步页面(不具有 Async="true" 属性)在执行异步操作期间,主页面线程不会被释放回线程池。

现在所有 Web 部件都在异步执行数据检索,就可以在标记为异步的所有页面中使用这些部件,并且我们知道,现在的响应时间不再是所有 Web 部件检索其数据所用时间之和,而是任一 Web 部件的最长需时。通过将页面标记为异步以及使用执行异步 I/O 的 Web 部件,能够提高网站潜在的可伸缩性。这是由于在等待数据期间,页面可以释放主请求线程为其他客户服务。此处的关键在于您是否采用 ASP.NET 2.0 来建立门户网站,您需要注意该版本中引入的所有新异步功能,并充分利用它们来改进应用的响应性和可伸缩性。有关 ASP.NET 2.0 中异步支持的详细信息,请参阅 Jeff Prosise 在 MSDN Magazine 2005 年 10 月号 Wicked Code 栏目发表的文章。

 

Fritz Onion 是 Pluralsight(一家 Microsoft .NET 培训提供商)的共同创始人之一,主持 Web 开发课题。Fritz 著有 Essential ASP.NET (Addison Wesley, 2003) 和即将出版的 Essential ASP.NET 2.0 (Addison Wesley, 2006)。要了解本作者,请登录 pluralsight.com/fritz。