在服务器端 Web 代码中使用线程和生成异步处理程序

Fritz Onion

本文假设您熟悉 C#、ASP.NET 和多线程处理

下载本文的代码: Threading.exe (121KB)

摘要

让开发人员感到幸运的是,在 ASP.NET 中进行线程处理要比在 ASP 中容易得多。在本文中,作者将考察 ASP.NET HTTP 管线中的线程处理,并解释如何在没有开发人员参与的情况下对线程进行有效管理。本文将考虑 ASP.NET 如何使用公共语言运行库线程池来为请求提供服务,考察用于处理程序、模块和应用程序的池机制,并说明 IIS 5.0 和 IIS 6.0 以及它们请求处理和线程分配的方法之间存在哪些差异。最后,本文将针对那些仍然需要在他们自己的应用程序中使用线程的开发人员来讨论如何和何时使用异步处理程序。

本页内容

ASP.NET 中的线程处理
IIS 5.0 和 6.0 中的线程调度
异步需要
异步处理程序
带有委托的异步处理程序
带有自定义线程的异步处理程序
带有自定义线程池的异步处理程序
异步页
小结

在传统的 ASP 中,开发人员面临许多他们不知道该如何处理的线程处理问题。因为 ASP 是在 COM 之上生成的,所以有关对象的线程处理要求,有一些非常特殊的规则。页所使用的对象需要进行单元线程处理,以获得最佳效率。相反地,存在于会话和应用程序状态中的对象需要具有上下文灵活性,并且能够聚合自由线程封送拆收器,以及防止它们的状态被并发访问。

但是,如果查看 ASP.NET 的文档,那么您将几乎找不到任何有关线程处理要求的信息。这是否意味着所有线程处理问题都已得到解决,并且 ASP.NET 开发人员可以无忧无虑地生成 .aspx 页和 Microsoft.NET Framework 类,而无需考虑并发问题呢?唔,是的,大多数情况下如此。

在本文中,我将研究 HTTP 管线中线程处理的详细信息,以及它是如何在不增加开发人员负担的前提下对线程进行有效管理的。我还将考察 ASP.NET 如何使用公共语言运行库 (CLR) 线程池来为请求提供服务,用于处理程序、模块和应用程序的池机制,以及 Microsoft Internet 信息服务 (IIS) 5.0 和 IIS 6.0 之间在请求处理和线程分配方面存在的一些差异。对于那些如果没有在自己的应用程序中使用线程就感觉不完整的开发人员,我还将介绍异步处理程序,并对如何以及何时使用它们进行讨论。

ASP.NET 中的线程处理

为了有效地为多个客户端请求提供服务,Web 服务器就要大量使用并发,这是通过启动多个进程以及/或者产生多个线程来为请求提供服务和执行工作实现的。ASP.NET 也不例外,它在每个辅助进程中使用多个线程来为请求提供服务。尽管如此,ASP.NET 开发人员自己并不需要关心众多与在多线程环境中进行开发有关的问题。因为总是在同一线程上为页请求提供服务,并且总是创建截然不同的 Page 类实例来为任何新请求提供服务。同时,还使用截然不同的应用程序和模块对象实例来为每个请求提供服务。但是,您有必要了解如何用线程来为请求提供服务,以防止自己就应用程序中的哪些对象可能被并发线程访问做出不正确的假设。

首先,ASP.NET 使用进程范围的 CLR 线程池为请求提供服务(有关 CLR 线程池的详细信息,请参阅本期中的 .NET 专栏)。该池的大小可以在 machine.config 的 processModel 元素中进行配置,并且被默认设置为 25 个辅助线程和 25 个 I/O 线程:

<processModel enable="true" 
              鈥⑩€⑩€?
            maxWorkerThreads="25" 
            maxIoThreads="25" />

线程的处理方式各不相同,具体取决于您是使用 IIS 5.0 还是 IIS 6.0(稍后我将对此进行讨论)。对于每个到来的请求,都会创建一个适当的 HttpApplication 派生类的新实例,对于该应用程序的关联模块也是如此。为了避免过于频繁地重新分配应用程序和模块,每个 AppDomain 都维护一个应用程序和模块池。应用程序池的最大大小与线程池的大小相同,因此,默认情况下,每个辅助进程最多有 25 个请求可以进行并发处理,每个请求都具有它自己的应用程序和模块集。图 1 显示了 ASP.NET 辅助进程工作期间的快照。在该方案中,辅助进程中有两个活动的应用程序,它们每一个都具有一个专用的 AppDomain。每个应用程序当前都在处理两个请求,并且每个应用程序都使用 CLR 线程池中的两个线程来为这些请求提供服务。

Aa686076.misthreadingfig01(zh-cn,MSDN.10).gif

图 1 HTTP 管线中的线程处理和池

该体系结构的多个方面可能会影响到 ASP.NET 应用程序的构建方式。首先,应用程序和模块被多次实例化的事实意味着您永远都不应当依赖于添加字段或其他状态的操作,这是因为它们并不像您可能认为的那样在多个请求之间共享。相反,应该使用管线中提供的许多状态储存库中的一个,例如,应用程序范围的缓存、会话状态袋、应用程序状态袋或 HttpContext 类的基于请求的项集合。另外,还可以使用静态数据(如果正确同步的话)。

默认情况下,大多数创建的以便为请求提供服务的处理程序都不会被汇集。您可以通过 IHttpHandler 的 IsReusable 属性汇集处理程序,甚至根据每个处理程序来控制汇集,但是只有以隐式方式汇集的处理程序才是您在不指派处理程序工厂的情况下编写的自定义处理程序。PageHandlerFactory 类不执行汇集,SimpleHandlerFactory 类(它实例化 .ashx 定义的处理程序)也不执行。因此,通常情况下,由适当的处理程序类的刚分配的实例来为每个请求提供服务(该实例在请求完成后被丢弃)。

IIS 5.0 和 6.0 中的线程调度

在 IIS 5.0 中,通常通过 I/O 线程或执行异步 I/O 的线程为请求提供服务,因为要使用对命名管道的异步写入来将请求调度给辅助进程。在辅助进程中为请求提供服务时,该进程通常使用执行异步读取的线程。尽管辅助进程中用来为请求提供服务的线程提取自进程范围的 CLR 线程池,但是与 I/O 完成端口联系在一起的线程池线程会错误地从 Thread 类的 IsThreadPoolThread 属性中返回假,因此不要依赖该属性。

虽然在 IIS 5.0 中,通常是在 I/O 线程中为请求提供服务,但当负载繁重时,会将某些请求传递给池中的正常辅助线程,因此会同时使用这两种类型的线程。CLR 线程池的默认大小为 25,对于 I/O 线程和辅助线程分别进行限制。您可以通过修改 machine.config 中的 processModel 元素,更改在 CLR 线程池中分配的线程个数的上限。

对于 IIS 6.0(它随附在 Windows Server 2003 中),情况发生了急剧的变化。首先,不再使用 inetinfo.exe 为 HTTP 请求提供服务(尽管它仍然能够为诸如 FTP 之类的其他协议提供服务)。相反,HTTP 请求在内核模式服务 http.sys 中排队,该服务负责将每个请求调度到适当的应用程序队列。此外,IIS 6.0 支持应用程序池,它是共享单个辅助进程的应用程序分组,现在被命名为 w3pwp.exe。应用程序池使您可以按照需要在任何给定服务器上具有任意多个互不相同的辅助进程,具体取决于您对 Web 应用程序之间的进程隔离的关注程度。

这一模式更改了在 ASP.NET 中处理请求的方式。Http.sys 直接在适当的进程中将每个请求进行排队,而不是将请求从 inetinfo.exe 调度到 ASP.NET 辅助进程。因而,现在所有请求都由从 CLR 线程池中提取的辅助线程提供服务,而绝不会由 I/O 线程提供服务。与 Windows Server 2003 上的 machine.config 中的说明相反,processModel 元素仍然用于初始化线程池的上限,尽管所有其他属性都被忽略(因为它们现在存储在 IIS 6.0 元数据库中)。

异步需要

有很多因素影响 Web 应用程序的可伸缩性。通常,每当多个并发操作请求对共享资源进行独占访问时,就会危及系统的可伸缩性。在系统中共享的资源列表的顶部是 CPU 本身(或者多处理器单元中的多个 CPU)。运行 ASP.NET 的服务器借助于线程池在多个并发请求中共享它的 CPU。线程池的用途是:在所分配的线程总数存在上限的情况下,有效地分配 CPU 时间以处理请求。这是一个需要具备的重要限制,因为如果在并发产生大量请求时无限制地创建线程,则很容易使系统疲于应付并最终停止工作。

但是,如果线程池中的线程用于执行并非大量消耗 CPU 的工作(例如,对远程数据库发出请求或者调用远程 Web 服务),则无需较高的 CPU 利用率就可能使线程池饱和。在这种情况下,线程池实际上会降低系统的可伸缩性,因为即使服务器不忙于处理其他请求,请求也可能被延迟(或者可能被拒绝)。

作为线程池充溢的示例,请考虑下面的 ASP.NET 页 slow.aspx,它人为地将它的响应延迟两秒钟(通过非 CPU 绑定活动),并打印出为它提供服务的线程:

<!-- File: slow.aspx -->
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Reflection" %>
<%@ Import Namespace="System.Threading" %>

<script runat="server">
  protected void Page_Load(object src, EventArgs e)
  {
    System.Threading.Thread.Sleep(2000);
    Response.Output.Write("SlowResponse, threadid={0}",
                    AppDomain.GetCurrentThreadId());
  }
</script>

另外一个 ASP.NET 页 fast.aspx 几乎完全相同,不同之处在于它不像 slow.aspx 那样休眠两秒钟:

<!-- File: fast.aspx -->
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Reflection" %>

<script runat="server">
  protected void Page_Load(object src, EventArgs e)
  {
    Response.Output.Write("FastResponse, threadid={0}",
                    AppDomain.GetCurrentThreadId());
  }
</script>

针对承载这两个页的服务器使用 Web Application Stress 工具时,可以测试在繁重的请求通信量负载下的平均响应时间。经过初步测试,在单独客户端计算机上的 100 个并发线程上只向 fast.aspx 页发出请求时,得出平均响应时间为 0.0824 秒,即相当于在一分钟时间间隔内为 52,190 个请求提供服务。但是,针对 fast.aspx 和 slow.aspx 运行客户端时,fast.aspx 请求的平均响应时间变为 4.47 秒(对于 slow.aspx 为 6.54 秒),而在一分钟内服务的请求总数减少到 1062。

尽管在包含所有成员时数字显著不同,但最令人烦恼的结果是为 fast.aspx 提供服务时平均多花了四秒钟,而您知道它只需花很少的时间进行处理。尽管该示例是专门设计的,但它显示了在具有服务起来相对缓慢的页并且这些页与响应速度更快的页争用相同请求线程时的最坏情况,即响应时间急剧增加。如果应用程序中的缓慢页由于较高的 CPU 使用率而反应缓慢时,则除了投入更多的硬件以外,您没有多少可以做的工作。但是,如果应用程序中的缓慢页是因为它们正在等待非 CPU 绑定操作完成而缓慢,则问题不在于 CPU 能力欠缺,而在于缓慢的请求正在充满线程池,以至于其他请求必须排队,直到某个线程被释放为止。

如果发现系统的可伸缩性正在受到 ASP.NET 线程池的不利影响,那么您有几种选择。您可以更改线程池的上限。正如您已经看到的那样,默认的上限被设置为 25 个辅助线程和 25 个 I/O 线程。当然,增加线程池中的上限在某种程度上是该问题的一个粗粒度解决方案。如果可以找到一个神奇的数字,以便保持较高的 CPU 利用率,并且只在服务器确实太忙而无法处理更多的请求时才延迟或拒绝请求,那么您就太幸运了。保持服务器真正繁忙所需的线程数量不是静态的,而是会基于诸如需求和进程复杂性等因素而波动。

另一个潜在的解决方案是仔细识别系统中的那些需要花费更长时间才能完成并且执行非 CPU 绑定操作的请求,分配不同的线程以为它们提供服务,释放原始的线程池线程以便为其他请求提供服务。这促使我对异步处理程序进行研究。

异步处理程序

尽管大多数 ASP.NET 页和处理程序在从进程范围的线程池中提取的线程上接受同步服务,但也可以创建以异步方式为请求提供服务的处理程序(甚至页)。异步处理程序实现了 IHttpAsyncHandler 接口,它派生自同步处理程序所实现的 IHttpHandler:

public interface IHttpAsyncHandler : IHttpHandler
{
  IAsyncResult BeginProcessRequest(HttpContext ctx,
                                   AsyncCallback cb, 
                                   object obj);
  void EndProcessRequest(IAsyncResult ar);
}

实现该接口的处理程序除了实现 IHttpHandler 的标准方法以外,还必须实现其他两个方法。第一个方法为 BeginProcessRequest,它由应用程序类调用,而不是直接调用 ProcessRequest。然后,由处理程序负责启动新的线程以处理该请求,然后立即从 BeginProcessRequest 方法中返回,并且传回对有效的 IAsyncResult 实例的引用,以便运行库知道操作已完成。另一个方法是 EndProcessRequest,在请求处理完成时调用该方法,并且可以用来根据需要清理任何已分配的资源。

带有委托的异步处理程序

如果您已经通过 .NET Framework 中的异步执行完成了某些工作,则您应该知道异步执行工作的最简单且最经常推荐的方式是使用异步委托调用。通过调用 BeginInvoke 异步调用委托时,会隐式使用进程范围的线程池中的线程执行委托函数。使用异步委托调用实现异步处理程序实际上非常简单,如图 2 所示。

我的 IHttpAsyncHandler.BeginProcessRequest 实现在我的 ProcessRequestDelegate 实例上调用了 BeginInvoke,而 ProcessRequestDelegate 实例已经通过 ProcessRequest 方法的引用进行了初始化。这具有在单独线程上执行 ProcessRequest(与请求线程不同)的效果。

运行与我在前面用 slow.aspx 页执行的测试相同的压力测试,但这一次使用新的 asyncDelegate.ashx 处理程序而不是 fast.aspx,将会产生下列结果。Fast.aspx 的平均响应时间为 4.14 秒,asyncDelegate.ashx 的平均响应时间为 7.86 秒,所服务的请求总数为 957。

令人失望的是,对于向 fast.aspx 发出的请求,响应时间并未切实改善。其原因在于,异步委托调用完全抵消了生成异步处理程序的用途,因为它从 ASP.NET 用来为请求提供服务的同一进程范围 CLR 线程池中提取线程。当确实将主请求线程返回到线程池后,另一个线程会被取出以执行异步委托执行,这意味着为了向其他请求提供服务而获得的净线程数为零,从而使得处理程序毫无用处。如果您使用 ThreadPool.QueueUserWorkItem 执行异步请求处理,则会遇到相同问题,因为它也从相同的进程范围 CLR 线程池中提取线程。

带有自定义线程的异步处理程序

要生成真正有效的异步处理程序,必须手动产生一个附加线程以响应 BeginProcessRequest。要生成成功的异步处理程序,有三个重要方面。首先,构建一个支持 IAsyncResult 的类,以便从 BeginProcessRequest 中返回。然后,产生线程以便异步执行请求处理。最后,通知 ASP.NET 您已经完成对请求的处理并且准备好返回响应。

我将通过生成一个支持 IAsyncResult 的类,开始构建异步处理程序。将从对 BeginProcessRequest 的调用中返回该类,随后会将该类传递到我的 EndProcessRequest 实现中,因此,除了有其他用途以外,该类还是一个有用的位置,可以存储我在对请求进行处理期间可能需要使用的特定于请求的状态。下面显示了要实现的完整 IAsyncResult 接口:

public interface IAsyncResult
{
  public object     AsyncState             { get; }
  public bool       CompletedSynchronously { get; }
  public bool       IsCompleted            { get; }
  public WaitHandle AsyncWaitHandle        { get; }
}

在该示例中,我将存储与请求关联的 HttpContext 对象的引用、传递到 BeginProcessRequest(我随后将调用它以完成请求)中的 AsyncCallback 委托的引用以及 BeginProcessRequest 的调用方可能使用的额外数据的一般对象引用。另一个必须在该类中实现的元素是一个同步对象,线程在该对象上等待以便在操作完成时收到通知。我将使用常见技术,即提供在请求完成时激发的 ManualResetEvent,但仅当有人请求它时才分配它。最后,我的类将具有一个名为 CompleteRequest 的方便方法,该方法将触发 ManualResetEvent(如果它被创建的话),调用 AsyncCallback 委托,并且将 IsCompleted 标志设置为真。AsyncRequestState 的完整类定义显示在图 3 中。

下一步是产生一个新线程以处理我的请求。我在这一新线程上调用的方法将需要访问我在刚刚显示的 AsyncRequestState 类中缓存的状态。遗憾的是,用来在 .NET Framework 中产生新线程的 ThreadStart 委托不采用任何参数。为了解决这一问题,我用作为数据成员缓存的必要状态(在这种情况下,只是对该请求的 AsyncRequestState 对象的引用)以及一个可以用来初始化 ThreadStart 委托的实例方法创建了另一个类。该类名为 AsyncRequest,它显示在图 4 中。请注意,我在该类中定义的 ProcessRequest 方法是将从手动创建的线程中调用的方法,并且当它完成时,它会通过调用 AsyncRequestState 对象上的 CompleteRequest 通知请求处理已经完成。

最后,我准备好生成异步处理程序类本身。该类(我将其称为 AsyncHandler)必须实现前面显示的 IHttpAsyncHandler 接口(它派生于 IHttpHandler,总共有四个方法)的所有方法。未使用 ProcessRequest 方法,并且永远不会调用它,因为我实现了 BeginProcessRequest。在 BeginProcessRequest 中,我创建了 AsyncRequestState 类的一个新实例,用 HttpContext 对象、AsyncCallback 委托和一般对象引用(每个都作为参数传入)将其初始化。然后,我准备了一个新的 AsyncRequest 对象(该对象用刚刚创建的 AsyncRequestState 对象进行了初始化),并且启动了一个新线程。在该示例中,EndProcessRequest 的实现将不会做任何事情,但是通常它可以用来执行任何清理工作或者执行最后时刻的附加响应。AsyncHandler 类定义显示在图 5 中。

当我生成该处理程序并将其注册为终结点(就像我对前面的处理程序所做的那样)时,它将成功地从 ASP.NET 的调用请求线程中异步处理请求。图 6 显示了将在生成映射到异步处理程序的请求时发生的事件序列。首先,应用程序类看到我的处理程序实现了 IHttpAsyncHandler,因此它不会调用同步的 ProcessRequest 方法,而是调用处理程序上的 BeginProcessRequest,并且传递当前上下文和异步回调委托以便进行完成回调。然后,处理程序创建一个新的 AsyncRequestState 对象,该对象用传递到 BeginProcessRequest 中的参数初始化。接下来,处理程序创建一个新的 AsyncRequest 对象(该对象用 AsyncRequestState 对象初始化),并且使用 AsyncRequest.ProcessRequest 方法作为入口点启动一个新线程。处理程序随后将 AsyncRequestState 对象返回到应用程序对象,并将调用线程返回到线程池,而手动创建的线程则继续处理请求。

Aa686076.misthreadingfig06(zh-cn,MSDN.10).gif

图 6 异步处理程序操作阶段 1

在 AsyncRequest 对象的 ProcessRequest 方法执行完它的冗长任务之后,它将立即调用 AsyncRequestState 类的 CompleteRequest 方法。而这进而将激发原来传递到 BeginProcessRequest 方法中的 AsyncCallback 委托,表明响应已经准备好,可以返回。AsyncCallback 委托首先要完成的是调用异步处理程序类上的 EndProcessRequest 方法。该方法返回以后,将立即通过发回准备好的响应来触发请求的完成。该处理全部发生在我在处理程序中创建的辅助线程上,而不是发生在线程池线程上。图 7 显示了在完成对异步处理程序的请求时所涉及到的步骤。

Aa686076.misthreadingfig07(zh-cn,MSDN.10).gif

图 7 异步处理程序操作阶段 2

当运行与我在前面针对 slow.aspx 页执行的测试相同的压力测试(这一次使用新的 AsyncThread.ashx 处理程序而不是 AsyncDelegate.aspx)时,产生了下列结果。Fast.aspx 的平均响应时间为 .012 秒,而 AsyncThread.ashx 的平均响应时间为 2.014 秒,所服务的请求总数为 6132。

对 fast.aspx 发出的请求的平均响应时间显著改善了,这是因为缓慢处理程序的服务请求未占用线程池。

带有自定义线程池的异步处理程序

对于该异步处理程序实现还有一个问题 — 它有可能创建无限数量的线程。如果向该异步处理程序发出大量请求,并且所有请求都需要花费大量时间提供服务,则最终很容易导致创建出的线程量多于基础操作系统能够处理的线程量。要解决该问题,我需要提供一个辅助线程池,以有限制的方式为请求提供服务。有关创建自定义线程池的详细信息超出了本文的范围,但是您可以从 http://staff.develop.com/woodring/dotnet/#threadpool 下载由 Mike Woodring 用 C# 编写的完整示例。该自定义线程池还支持调用上下文和 HttpContext 的传播,这意味着您可以在异步方法中访问 HttpContext.Current(这与前面的示例不同)。实现线程池的类称为 DevelopMentor.ThreadPool,并且它的构造函数采用初始线程计数、最大线程计数和一个用于赋予该线程池名称的字符串作为参数。要通过该线程池发出异步请求,请调用 PostRequest 并传入对 WorkRequestDelegate 的引用。

要重写该异步处理程序以使用该自定义线程池,将有两项只是为了产生新线程而进行的重大更改。首先,向处理程序类中添加一个 DevelopMentor.ThreadPool 类型的静态成员,并且添加一个静态构造函数以针对每个 AppDomain 将线程池初始化一次。然后,更改 BeginProcessRequest 的实现,以便用 WorkRequestDelegate 的新实例调用线程池上的 PostRequest(参见图 8)。如果您运行与在前面针对 slow.aspx 页执行的测试相同的压力测试(这一次使用新的 AsyncPool.ashx 处理程序而不是 AsyncThread.aspx)时,则将得到下列结果。Fast.aspx 的平均响应时间为 .055 秒,而 AsyncThread.ashx 的平均响应时间为 12.24 秒,所服务的请求的总数为 1042。

请注意,向 fast.aspx 发出的请求的平均响应时间仍然最小,并且不再有创建无限数量的线程的危险。有一些与维护辅助线程池有关的附加管理工作,从而导致针对缓慢处理程序的请求时间延长。

异步页

迄今为止,所有异步请求处理示例都涉及到生成自定义处理程序(在这种情况下,使用 .ashx 文件)。在很多情况下,可以识别 ASP.NET 应用程序中也适合进行异步执行的常规页 (.aspx),因为它们会执行需要花费大量时间的非 CPU 绑定任务。这样的页也是 ASP.NET 线程池的潜在瓶颈。Page 类本身实现了 IHttpHandler 接口,因而以同步方式为请求提供服务。要将给定的 .aspx 页更改为以异步方式为请求提供服务,需要在 Page 派生类中实现 IHttpAsyncHandler,然后使用本文介绍的技术,在备用线程上调用 Page 类的 ProcessRequest 方法。

使任何页成为异步页的一种方便方式是:创建一个备用基类以便派生主要页,然后使用 @Page 指令的 Inherits 属性将该备用基类指定为派生该页的基类。图 9 中显示的 AsyncPage 类定义就定义了这样一个基类。请注意,该实现使用前面显示的自定义线程池,以异步方式服务针对该页的请求。BeginProcessRequest 的这一实现用一个新的 WorkRequestDelegate(它用我的 ProcessRequest 重载的引用进行了初始化)调用了线程池的 PostRequest 方法。在重载中,我调用了 ProcessRequest 的基类范围(用 IHttpHandler 接口中定义的原始签名),它执行页的标准请求处理,只是这一次它将在线程池线程而不是主请求线程上执行(参见图 9)。

作为使用这一新的异步页基类的示例,请考虑下面这个从 AsyncPage 类继承的 ASPX 页。可以采用类似的方式将该基类应用于任何 ASPX 页,以将其转换为异步执行的页:

<!-- AsyncPage.aspx -->
<%@ Page Language="C#" 
    Inherits="EssentialAspDotNet.HttpPipeline.AsyncPage" %>
<%@ import Namespace="System.Reflection" %>
<%@ import Namespace="System.Threading" %>

<script runat="server">
protected void Page_Load(object src, EventArgs e)
{
   System.Threading.Thread.Sleep(2000);
   Response.Output.Write("AsyncPage, {0}",
                         AppDomain.GetCurrentThreadId);
}
</script>

用 fast.aspx 和异步页 AsyncPage.aspx 执行压力测试的结果如下所示(这些结果与使用自定义线程池的异步自定义处理程序相匹配)。Fast.aspx 的平均响应时间为 .054 秒,而 AsyncPage.aspx 的平均响应时间为 11.92 秒,所服务的请求的总数为 965。

小结

生成异步处理程序而不是同步处理程序实际上会使请求处理增加额外的开销,因此应当花时间确定是否确实需要这些类型的处理程序的异步功能。异步处理程序的用途是在该处理程序处理原始请求的同时,释放 ASP.NET 线程池线程以服务其他请求。只有在服务请求的工作需要大量非 CPU 绑定时间才能完成时,这一功能才有意义。例如,如果请求的完成取决于很多远程过程调用(或者 Web 服务调用)的完成,则适合于实现异步处理程序。如果生成异步处理程序以服务大量消耗 CPU 的请求,则只会增加与 ASP.NET 线程池线程竞争的线程,并且实际上可能会延长请求的总体处理时间。

相关文章,请参阅:

The ASP Column: HTTP Modules
HTTP Pipelines: Securely Implement Request Processing, Filtering, and Content Redirection with HTTP Pipelines in ASP.NET
IIS 6.0: New Features Improve Your Web Server's Performance, Reliability, and Scalability
有关背景信息,请参阅:
ASP.NET QuickStart Tutorial
Building Secure ASP.NET Applications: Authentication, Authorization, and Secure Communication

Fritz Onion 是 DevelopMentor 的技术人员,他在这里研究、执教和编写有关 .NET Framework(包括 Essential ASP.NET)的课程。本文改编自 Essential ASP.NET with Examples in C# (Addison-Wesley, 2003)。讨厌分号的读者可以阅读另一个版本:Essential ASP.NET with Examples in Visual Basic .NET

转到原英文页面