通过 ASP.NET 异步编程实现可扩展的应用程序
2014-03-05 14:21
381 查看
http://msdn.microsoft.com/zh-cn/magazine/cc163463.aspx
通过 ASP.NET 异步编程实现可扩展的应用程序
Jeff Prosise
代码下载位置: WickedCode2007_03.exe (202
KB)
Browse
the Code Online
目录
异步页面
异步 HTTP 处理程序
异步 HTTP 模块
总结
您想了解秘密吗?讳莫如深,不可言传的秘密?一旦揭示,将在 ASP.NET 社区引起巨大的反响,并使 Microsoft 的反对者发出“啊哈!”的惊叹,对吗?
多数使用 ASP.NET 构建的网站没有良好的可扩展性。它们受到自我强加的“玻璃天花板”的制约,这种束缚限制了它们每秒可处理的请求的数量。这些站点的扩展性一直良好,直到流量提升到这一无形限制时。然后吞吐量开始下降。很快,请求开始失败,通常返回“服务器不可用”错误。
《MSDN®杂志》曾多次就其根本原因进行过讨论。ASP.NET
使用公共语言运行库 (CLR) 线程池中的线程来处理请求。只要在线程池中存在可用线程,ASP.NET 调度传入请求就不会有任何麻烦。但是一旦线程池处于饱和状态(即所有池中的线程忙于处理请求,而没有可用的线程),则新的请求必须等待线程可用。如果这种僵局变得相当严重、队列到达容量限制,ASP.NET 将束手无策,对于新的请求只能做出“拒绝”响应。
一种解决方法是提高线程池的上限,以创建更多的线程。这是当其客户报告频繁遇到“服务器不可用”错误时,开发人员经常采取的方法。另一种经常采用的方法是放弃出现问题的硬件,向 Web 场中添加更多的服务器。但是,增加线程数或服务器数并不能从根本上解决这一问题。实际上,它仅仅暂时缓解了存在的设计问题,并非存在于 ASP.NET 中,而是实际站点实现中存在的问题。对于不能扩展的应用程序,实际的问题是线程的缺乏。不能有效使用已存在的线程是问题的所在。
真正可扩展的 ASP.NET 网站充分利用了线程池。这意味着可确保请求处理线程执行代码,而非等待 I/O 完成。如果由于所有线程都在消耗 CPU 而造成线程池饱和,除了添加服务器,您几乎无计可施。
然而,多数 Web 应用程序可以与数据库、Web 服务或其他外部实体进行通话,并通过强制线程池等待完成数据库查询、Web 服务调用和其他 I/O 操作来限制可扩展性。针对数据驱动的网页的查询可能要花费千分之几秒来执行代码,花几秒钟等待数据库查询返回。当查询未完成时,分配给请求的线程无法服务于其他的请求。这就是所谓的玻璃屋顶。如果您要构建具有高度可扩展性的网站,这种情况是您必须避免的。请记住:当涉及吞吐量时,除非处理得当,否则 I/O 会成为大问题。
当然,如果 I/O 没有破坏线程池,则算不上大问题。ASP.NET 支持三种可作为防破坏代理的异步编程模型。对于社区而言,这些模型大都未知,部分原因在于缺乏相关文档。了解如何以及何时使用这些模型对于构建先进的网站绝对至关重要。
异步页面
ASP.NET 支持的这三种异步编程模型中,首要的、通常也是最有用的是异步页面。在这三种模型中,这是唯一针对 ASP.NET 2.0 的。其他支持的模型都是针对版本 1.0 的。
在此,我不再详细介绍异步页面,因为在 2005 年 10 月期的杂志中,我曾对此进行过讨论。(msdn.microsoft.com/msdnmag/issues/05/10/WickedCode)。结论是:如果您有一些页面要执行相对较长的
I/O 操作,它们就应成为异步页面。如果某页面查询数据库,花了 5 秒钟返回(因为它既返回大量数据,又通过大量加载的连接将目标锁定到远程数据库),线程分配给该请求的 5 秒钟不可用于其他请求。如果每个请求都照此处理,应用程序将会很快陷入停顿。
图 1 显示了异步页面是如何解决这一问题的。当请求到达时,由 ASP.NET 为其分配一个线程。该请求开始在该线程中进行处理,当选择数据库时,请求将启动异步 ADO.NET 查询,并将线程返回到线程池中。当查询完成时,ADO.NET 回调到 ASP.NET,ASP.NET 从线程池中调出另一个线程,并恢复处理请求。
图 1 有效的异步页面 (单击该图像获得较大视图)
查询未完成时,线程池中的任何线程均未使用,以确保所有线程均可用于传入的请求。异步处理的请求不能被快速执行。但其他请求可更快地执行,因为它们不必等待线程可用。在进入管道时,请求可引起较少的延迟,整体吞吐量会被提升。
图 2 显示了根据 SQL Server™ 数据库执行数据绑定的异步页面的代码隐藏类。Page_Load 方法调用 AddOnPreRenderCompleteAsync 以注册开始和结束处理程序。在请求生存期的末期,ASP.NET 调用 Begin 方法,该方法将启动异步 ADO.NET
查询并即刻返回,于是,分配给该请求的线程将返回线程池。当 ADO.NET 表明查询已经结束时,ASP.NET 将从线程池中检索线程(不必和以前使用的相同),并调用 End 方法。End 方法获得查询结果,请求的其余部分在执行 End 方法的线程中正常执行。
Figure 2 异步页面
在图 2 中未显示的内容是 ASPX 的 Page 指令中的 Async="true" 属性。异步页面应能够:提示 ASP.NET 在页面中实现 IHttpAsyncHandler 接口(稍后将详细介绍)。同样未在图
2 中显示的是数据库连接字符串,该字符串包含自己的 Async="true" 属性,这样,ADO.NET 就知道要执行异步查询了。
AddOnPreRenderCompleteAsync 是构建异步页面的一种方法。另一种方法是调用 RegisterAsyncTask。与 AddOnPreRenderCompleteAsync 方法相比,这种方法具有一些优势,最重要的是它简化了在一个请求中执行多个异步 I/O 操作的任务。有关于此的详细信息,请参阅 2005 年 10 月期的“超酷代码”部分。
异步 HTTP 处理程序
ASP.NET 中的第二个异步编程模型是异步 HTTP 处理程序。HTTP 处理程序是一个作为请求终结点的对象。例如,对 ASPX 文件的请求由针对 ASPX 文件的 HTTP 处理程序处理。同样,对 ASMX 文件的请求由知道如何处理 ASMX 服务的 HTTP 处理程序处理。事实上,ASP.NET 拥有针对多种文件类型的 HTTP 处理程序。在 web.config 主文件的 <httpHandlers> 部分(在 ASP.NET 1.x中,其位于
machine.config 中),您可以看到这些文件类型和相应的 HTTP 处理程序。
通过编写自定义 HTTP 处理程序,您可以扩展 ASP.NET 以支持其他文件类型。但是,更有趣的一点是,您可以在 ASHX 文件中部署 HTTP 处理程序,并将它们用作 HTTP 请求的目标。这是构建动态生成图像或从数据库中检索图像的 Web 端点的正确方法。您只需将 <img> 标记(或 Image 控件)包含在页面中,并将其指向创建或获取图像的 ASHX。将目标锁定到带有请求的 ASHX 文件比将目标锁定到 ASPX 文件更有效,因为 ASHX 文件在处理时开销更少。
根据定义,HTTP 处理程序可实现 IHttpHandler 接口。实现该接口的处理程序不能同步进行处理。图 3 中的 ASHX 文件包含一个这类 HTTP 处理程序。在运行时,TerraServiceImageGrabber 在 Microsoft® TerraServer
Web 服务之外进行多次调用以将城市和州转换为经度和纬度,检索卫星图像(如同一块块“瓷砖”),然后将图像拼接在一起形成指定位置的复合图像。
Figure 3 同步
HTTP 处理程序
结果令人印象深刻。但这有一个问题。TerraServiceImageGrabber 是如何避免编写 HTTP 处理程序的完美示例。想一想。TerraServiceImageGrabber 需要几秒钟(至少)完成其所有 Web 服务调用并处理结果。大部分时间仅仅花费在等待 Web 服务调用完成上。对于 ASHX 文件的重复请求会转瞬间耗尽 ASP.NET 线程池,阻止应用程序中其他页面的使用(或者至少使它们排队等待线程可用)。您不能用这种方法构建可扩展的应用程序,除非您扩展了硬件。但是,当使用正确编写的软件通过一台服务器就能处理负载时,为什么还要将成千上万的资金耗费在
Web 场上呢?
图 4 有效的 TerraServiceImageGrabber (单击该图像获得较大视图)
HTTP 处理程序不必是同步的。通过实现 IHttpAsyncHandler 接口,该接口本身可从 IHttpHandler 派生出来,HTTP 处理程序可以是异步的。如果正确使用,异步处理程序可更有效地利用 ASP.NET 线程。这可采用与异步页面相同的方式来完成。事实上,异步页面可利用在 ASP.NET 中将异步页面日期提前的异步处理程序支持。
图 5 包含图 3 所示的处理程序的异步版本。Async-TerraServiceImageGrabber
稍微有点复杂,但具有更高的可扩展性。
Figure 5 异步
HTTP 处理程序
当 ASP.NET 调用处理程序的 BeginProcessRequest 方法时,开始异步处理。通过 TerraService 代理的 BeginConvertPlaceToLonLatPt 方法,BeginProcessRequest 可对 TerraService 进行异步调用。然后,分配给该请求的线程返回线程池中。异步调用完成时,另一个线程被从线程池中调出以执行 ConvertPlaceToLonLatCompleted 方法。该线程会检索上次调用的结果,进行自己的异步调用,然后返回线程池。这种模式不断重复直至所有异步调用完成,此时,调用处理程序的
EndProcessRequest 方法,产生的位图被返回给请求者。
要阻止 EndProcessRequest 直至最后的 Web 服务调用完成,AsyncTerraServiceImageGrabber 返回来自 BeginProcessRequest 的 IAsyncResult 的自我实现。如果它要返回由 BeginConvertPlaceToLonLatPt 返回的 IAsyncResult,则在第一个 Web 服务调用完成时,需调用 EndProcessRequest(并终止请求)。
实现 IAsyncResult 和 TerraServiceAsyncResult 的类具有可随时调用以完成请求的公共 CompleteCall 方法。通常,只有在最后的 Web 服务调用完成后,AsyncTerraServiceImageGrabber 才调用 CompleteCall。不过,如果在 BeginProcessRequest 和 EndProcessRequest 之间执行的某一方法抛出异常,处理程序将异常缓存在私有字段 (_ex) 中,调用 CompleteCall 以终止请求,然后从 EndProcessReques
中重新抛出异常。否则,异常将丢失,请求将无法完成。
由于 AsyncTerraServiceImageGrabber 使用 ASP.NET 线程的时间只是处理请求所需的总时间的一小部分,因此,AsyncTerraServiceImageGrabber 比其同步版的同类方法具有更高的可扩展性。大部分时间里,它只是等待异步 Web 服务调用完成。
理论上,AsyncTerraServiceImageGrabber 还胜过 TerraServiceImageGrabber,因为它不是顺序地重复调用 TerraService's GetTile 方法,而是并行调用。不过,实际上,每次只有两个针对给定 IP 地址的出站调用可以被挂起,除非您提高了运行库的默认 maxconnection 设置:
其他配置设置也可影响并发。有关详细信息,请参考知识库文章“从 ASP.NET 应用程序进行 Web 服务请求时出现的争用、性能不佳和死锁等问题”(support.microsoft.com/kb/821268)。
即使每次只执行一个调用,但 AsyncTerraServiceImageGrabber 并不比 TerraServiceImageGrabber 差。它的设计非常出色,因为它尽可能有效地使用了 ASP.NET 线程。
异步 HTTP 模块
您在 ASP.NET 中可能利用的第三个异步编程模型是异步 HTTP 模块。HTTP 模块是位于 ASP.NET 管道中的对象,在管道中,它可以查看甚至修改传入请求和传出响应。ASP.NET 中的许多主要服务都是以 HTTP 模块的形式实现的,包括身份验证、授权和输出缓存。通过编写自定义 HTTP 模块并将它们插入管道,您可以扩展 ASP.NET。当您这样做的时候,一定要认真考虑这些 HTTP 模块是否应当是异步的。
图 6 包括称为 RequestLogModule 的简单、同步 HTTP 模块的源代码,它在名为 RequestLog.txt 的文本文件中记录了传入请求。在站点的 App_Data 目录下创建该文件,这样用户就无法浏览它。(要注意 ASP.NET 作为安全主体的运行(例如,ASPNET
或网络服务)必须写入对 App_Data 的使用权限。)该模块实现 IHttpModule 接口,这是 HTTP 模块的唯一要求。加载该模块时,其 Init 方法会为 HttpApplication.PreRequestHandlerExecute 事件注册一个处理程序,该程序从每个请求的管道中被触发。事件处理程序打开 RequestLog.txt(或在该文件不存在的情况下创建一个),然后将一行包含关于当前请求的有针对性的信息写入其中,包括请求到达的时间和日期、请求者的用户名(如果请求是要进行身份验证的,或者如果身份验证关闭,则要包含请求者的
IP 地址),以及请求的 URL。该模块在 web.config 的 <httpModules> 部分进行注册,以便在每次应用程序启动时,提示 ASP.NET 加载该文件。
Figure 6 同步
HTTP 模块
RequestLogModule 存在两方面的问题。首先,每次请求时均要执行 I/O 文件。其次,它使用请求处理线程来执行 I/O,否则,线程可能被用于为其他传入请求服务。由于简单,该模块会导致吞吐量损失。通过批处理 I/O 文件操作,您可能会缓解延迟,更好的方法是使模块异步(或者最好批处理 I/O 文件并使模块异步)。
图 7 显示了异步版本的 RequestLogModule。调用 AsyncRequestLogModule 后,它将执行完全相同的工作,并将分配给请求的线程返回线程池,然后写入文件。当写入完成时,从线程池中调出新的线程,用于完成请求。
Figure 7 异步
HTTP 模块
如何使 AsyncRequestLogModule 异步?其 Init 方法调用 HttpApplication.AddOnPreRequestHandlerExecuteAsync 以便为 PreRequestHandlerExecute 事件注册 Begin 和 End 方法。HttpApplication 类包含针对其他 per-request 事件的其他 AddOn 方法。例如,HTTP 模块可以调用 AddOnBeginRequestAsync 以便为 BeginRequest 事件注册异步处理程序。AsyncRequestLogModule
的 BeginPreRequestHandlerExecute 方法使用 Framework 的 FileStream.BeginWrite 方法来开始异步写入。BeginPreRequestHandlerExecute 返回时,线程返回线程池。
AsyncRequestLogModule 包含一些值得特别一提的线程同步逻辑。运行在多个线程中的多个请求可能要同时写入日志文件。为了确保并发写入不会相互覆盖,AsyncRequestLogModule 在由所有模块实例共享的私有字段中保存了下一个写入在文件中的位置 (_position)。每次调用 BeginWrite 之前,模块从字段中读取该位置并更新字段以指向要写入该文件的内容的第一个字节。读取并更新 _position 的逻辑包含在 lock 语句中,这样每次就有不止一个线程可执行它。这防止了在一个线程有机会更新位置之前,另一个线程读取该位置。
总结
异步编程是尽可能高效地使用 ASP.NET 线程池来构建扩展性更强的应用程序的一种很好的方法。以往,我很少看到 ASP.NET 开发人员使用异步编程模型,部分原因在于他们并不知道存在这些模型。不要让稀疏文档成为您的“拦路虎”,从现在起就开始异步思考,今后您将会构建出更好的应用程序。
请注意,本文提供了 C# 和 Visual Basic® 版本的可下载示例代码。我常常收到要求提供 Visual
Basic 版示例的电子邮件。这一次,您不必再问了,我已经提供了该版本的示例!
通过 ASP.NET 异步编程实现可扩展的应用程序
Jeff Prosise
代码下载位置: WickedCode2007_03.exe (202
KB)
Browse
the Code Online
目录
异步页面
异步 HTTP 处理程序
异步 HTTP 模块
总结
您想了解秘密吗?讳莫如深,不可言传的秘密?一旦揭示,将在 ASP.NET 社区引起巨大的反响,并使 Microsoft 的反对者发出“啊哈!”的惊叹,对吗?
多数使用 ASP.NET 构建的网站没有良好的可扩展性。它们受到自我强加的“玻璃天花板”的制约,这种束缚限制了它们每秒可处理的请求的数量。这些站点的扩展性一直良好,直到流量提升到这一无形限制时。然后吞吐量开始下降。很快,请求开始失败,通常返回“服务器不可用”错误。
《MSDN®杂志》曾多次就其根本原因进行过讨论。ASP.NET
使用公共语言运行库 (CLR) 线程池中的线程来处理请求。只要在线程池中存在可用线程,ASP.NET 调度传入请求就不会有任何麻烦。但是一旦线程池处于饱和状态(即所有池中的线程忙于处理请求,而没有可用的线程),则新的请求必须等待线程可用。如果这种僵局变得相当严重、队列到达容量限制,ASP.NET 将束手无策,对于新的请求只能做出“拒绝”响应。
一种解决方法是提高线程池的上限,以创建更多的线程。这是当其客户报告频繁遇到“服务器不可用”错误时,开发人员经常采取的方法。另一种经常采用的方法是放弃出现问题的硬件,向 Web 场中添加更多的服务器。但是,增加线程数或服务器数并不能从根本上解决这一问题。实际上,它仅仅暂时缓解了存在的设计问题,并非存在于 ASP.NET 中,而是实际站点实现中存在的问题。对于不能扩展的应用程序,实际的问题是线程的缺乏。不能有效使用已存在的线程是问题的所在。
真正可扩展的 ASP.NET 网站充分利用了线程池。这意味着可确保请求处理线程执行代码,而非等待 I/O 完成。如果由于所有线程都在消耗 CPU 而造成线程池饱和,除了添加服务器,您几乎无计可施。
然而,多数 Web 应用程序可以与数据库、Web 服务或其他外部实体进行通话,并通过强制线程池等待完成数据库查询、Web 服务调用和其他 I/O 操作来限制可扩展性。针对数据驱动的网页的查询可能要花费千分之几秒来执行代码,花几秒钟等待数据库查询返回。当查询未完成时,分配给请求的线程无法服务于其他的请求。这就是所谓的玻璃屋顶。如果您要构建具有高度可扩展性的网站,这种情况是您必须避免的。请记住:当涉及吞吐量时,除非处理得当,否则 I/O 会成为大问题。
当然,如果 I/O 没有破坏线程池,则算不上大问题。ASP.NET 支持三种可作为防破坏代理的异步编程模型。对于社区而言,这些模型大都未知,部分原因在于缺乏相关文档。了解如何以及何时使用这些模型对于构建先进的网站绝对至关重要。
异步页面
ASP.NET 支持的这三种异步编程模型中,首要的、通常也是最有用的是异步页面。在这三种模型中,这是唯一针对 ASP.NET 2.0 的。其他支持的模型都是针对版本 1.0 的。
在此,我不再详细介绍异步页面,因为在 2005 年 10 月期的杂志中,我曾对此进行过讨论。(msdn.microsoft.com/msdnmag/issues/05/10/WickedCode)。结论是:如果您有一些页面要执行相对较长的
I/O 操作,它们就应成为异步页面。如果某页面查询数据库,花了 5 秒钟返回(因为它既返回大量数据,又通过大量加载的连接将目标锁定到远程数据库),线程分配给该请求的 5 秒钟不可用于其他请求。如果每个请求都照此处理,应用程序将会很快陷入停顿。
图 1 显示了异步页面是如何解决这一问题的。当请求到达时,由 ASP.NET 为其分配一个线程。该请求开始在该线程中进行处理,当选择数据库时,请求将启动异步 ADO.NET 查询,并将线程返回到线程池中。当查询完成时,ADO.NET 回调到 ASP.NET,ASP.NET 从线程池中调出另一个线程,并恢复处理请求。
图 1 有效的异步页面 (单击该图像获得较大视图)
查询未完成时,线程池中的任何线程均未使用,以确保所有线程均可用于传入的请求。异步处理的请求不能被快速执行。但其他请求可更快地执行,因为它们不必等待线程可用。在进入管道时,请求可引起较少的延迟,整体吞吐量会被提升。
图 2 显示了根据 SQL Server™ 数据库执行数据绑定的异步页面的代码隐藏类。Page_Load 方法调用 AddOnPreRenderCompleteAsync 以注册开始和结束处理程序。在请求生存期的末期,ASP.NET 调用 Begin 方法,该方法将启动异步 ADO.NET
查询并即刻返回,于是,分配给该请求的线程将返回线程池。当 ADO.NET 表明查询已经结束时,ASP.NET 将从线程池中检索线程(不必和以前使用的相同),并调用 End 方法。End 方法获得查询结果,请求的其余部分在执行 End 方法的线程中正常执行。
Figure 2 异步页面
using System; using System.Data; using System.Data.SqlClient; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.Configuration; public partial class AsyncDataBind : System.Web.UI.Page { private SqlConnection _connection; private SqlCommand _command; private SqlDataReader _reader; protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { AddOnPreRenderCompleteAsync( new BeginEventHandler(BeginAsyncOperation), new EndEventHandler(EndAsyncOperation) ); } } IAsyncResult BeginAsyncOperation (object sender, EventArgs e, AsyncCallback cb, object state) { string connect = WebConfigurationManager.ConnectionStrings ["AsyncPubs"].ConnectionString; _connection = new SqlConnection(connect); _connection.Open(); _command = new SqlCommand( "SELECT title_id, title, price FROM titles", _connection); return _command.BeginExecuteReader (cb, state); } void EndAsyncOperation(IAsyncResult ar) { _reader = _command.EndExecuteReader(ar); } protected void Page_PreRenderComplete(object sender, EventArgs e) { Output.DataSource = _reader; Output.DataBind(); } public override void Dispose() { if (_connection != null) _connection.Close(); base.Dispose(); } }
在图 2 中未显示的内容是 ASPX 的 Page 指令中的 Async="true" 属性。异步页面应能够:提示 ASP.NET 在页面中实现 IHttpAsyncHandler 接口(稍后将详细介绍)。同样未在图
2 中显示的是数据库连接字符串,该字符串包含自己的 Async="true" 属性,这样,ADO.NET 就知道要执行异步查询了。
AddOnPreRenderCompleteAsync 是构建异步页面的一种方法。另一种方法是调用 RegisterAsyncTask。与 AddOnPreRenderCompleteAsync 方法相比,这种方法具有一些优势,最重要的是它简化了在一个请求中执行多个异步 I/O 操作的任务。有关于此的详细信息,请参阅 2005 年 10 月期的“超酷代码”部分。
异步 HTTP 处理程序
ASP.NET 中的第二个异步编程模型是异步 HTTP 处理程序。HTTP 处理程序是一个作为请求终结点的对象。例如,对 ASPX 文件的请求由针对 ASPX 文件的 HTTP 处理程序处理。同样,对 ASMX 文件的请求由知道如何处理 ASMX 服务的 HTTP 处理程序处理。事实上,ASP.NET 拥有针对多种文件类型的 HTTP 处理程序。在 web.config 主文件的 <httpHandlers> 部分(在 ASP.NET 1.x中,其位于
machine.config 中),您可以看到这些文件类型和相应的 HTTP 处理程序。
通过编写自定义 HTTP 处理程序,您可以扩展 ASP.NET 以支持其他文件类型。但是,更有趣的一点是,您可以在 ASHX 文件中部署 HTTP 处理程序,并将它们用作 HTTP 请求的目标。这是构建动态生成图像或从数据库中检索图像的 Web 端点的正确方法。您只需将 <img> 标记(或 Image 控件)包含在页面中,并将其指向创建或获取图像的 ASHX。将目标锁定到带有请求的 ASHX 文件比将目标锁定到 ASPX 文件更有效,因为 ASHX 文件在处理时开销更少。
根据定义,HTTP 处理程序可实现 IHttpHandler 接口。实现该接口的处理程序不能同步进行处理。图 3 中的 ASHX 文件包含一个这类 HTTP 处理程序。在运行时,TerraServiceImageGrabber 在 Microsoft® TerraServer
Web 服务之外进行多次调用以将城市和州转换为经度和纬度,检索卫星图像(如同一块块“瓷砖”),然后将图像拼接在一起形成指定位置的复合图像。
Figure 3 同步
HTTP 处理程序
<%@ WebHandler Language="C#" Class="TerraServiceImageGrabber" %> using System; using System.Web; using System.Drawing; using System.Drawing.Imaging; using System.IO; public class TerraServiceImageGrabber : IHttpHandler { public void ProcessRequest (HttpContext context) { // Extract user input from the query string string city = context.Request["City"]; string state = context.Request["State"]; string scale = context.Request["Scale"]; // If city or state wasn't specified, throw an exception if (String.IsNullOrEmpty(city) || String.IsNullOrEmpty(state)) throw new ArgumentException( "City and state must be specified via query string"); // Determine the scale Scale res = Scale.Scale8m; if (!String.IsNullOrEmpty (scale)) { switch (scale) { case "1": res = Scale.Scale1m; break; case "2": res = Scale.Scale2m; break; case "4": res = Scale.Scale4m; break; case "8": res = Scale.Scale8m; break; case "16": res = Scale.Scale16m; break; case "32": res = Scale.Scale32m; break; } } // Generate the requested image using(Bitmap bitmap = GetTiledImage(city, state, res, 900, 600)) { // Set the response's content type context.Response.ContentType = "image/jpeg"; // Write the image to the HTTP response bitmap.Save(context.Response.OutputStream, ImageFormat.Jpeg); } } public bool IsReusable { get { return true; } } private Bitmap GetTiledImage (string city, string state, Scale scale, int cx, int cy) { // Instantiate the TerraService proxy TerraService ts = new TerraService (); // Get the latitude and longitude of the requested city Place place = new Place (); place.City = city; place.State = state; place.Country = "USA"; LonLatPt point = ts.ConvertPlaceToLonLatPt (place); // Compute the parameters for a bounding box AreaBoundingBox abb = ts.GetAreaFromPt (point, Theme.Photo, scale, cx, cy); // Create an image to fit the bounding box Bitmap bitmap = new Bitmap (cx, cy, PixelFormat.Format32bppRgb); using(Graphics g = Graphics.FromImage(bitmap)) { int x1 = abb.NorthWest.TileMeta.Id.X; int y1 = abb.NorthWest.TileMeta.Id.Y; int x2 = abb.NorthEast.TileMeta.Id.X; int y2 = abb.SouthWest.TileMeta.Id.Y; for (int x=x1; x<=x2; x++) { for (int y=y1; y>=y2; y--) { TileId tid = abb.NorthWest.TileMeta.Id; tid.X = x; tid.Y = y; using(Image tile = Image.FromStream( new MemoryStream(ts.GetTile(tid)))) { g.DrawImage(tile, (x - x1) * tile.Width - (int) abb.NorthWest.Offset.XOffset, (y1 - y) * tile.Height - (int) abb.NorthWest.Offset.YOffset, tile.Width, tile.Height); } } } } // Return the image return bitmap; } }
结果令人印象深刻。但这有一个问题。TerraServiceImageGrabber 是如何避免编写 HTTP 处理程序的完美示例。想一想。TerraServiceImageGrabber 需要几秒钟(至少)完成其所有 Web 服务调用并处理结果。大部分时间仅仅花费在等待 Web 服务调用完成上。对于 ASHX 文件的重复请求会转瞬间耗尽 ASP.NET 线程池,阻止应用程序中其他页面的使用(或者至少使它们排队等待线程可用)。您不能用这种方法构建可扩展的应用程序,除非您扩展了硬件。但是,当使用正确编写的软件通过一台服务器就能处理负载时,为什么还要将成千上万的资金耗费在
Web 场上呢?
图 4 有效的 TerraServiceImageGrabber (单击该图像获得较大视图)
HTTP 处理程序不必是同步的。通过实现 IHttpAsyncHandler 接口,该接口本身可从 IHttpHandler 派生出来,HTTP 处理程序可以是异步的。如果正确使用,异步处理程序可更有效地利用 ASP.NET 线程。这可采用与异步页面相同的方式来完成。事实上,异步页面可利用在 ASP.NET 中将异步页面日期提前的异步处理程序支持。
图 5 包含图 3 所示的处理程序的异步版本。Async-TerraServiceImageGrabber
稍微有点复杂,但具有更高的可扩展性。
Figure 5 异步
HTTP 处理程序
<%@ WebHandler Language="C#" Class="AsyncTerraServiceImageGrabber" %> using System; using System.Web; using System.Drawing; using System.Drawing.Imaging; using System.Threading; using System.IO; public class AsyncTerraServiceImageGrabber : IHttpAsyncHandler { private TerraService _ts; private TerraServiceAsyncResult _ar; private Scale _scale = Scale.Scale8m; private AreaBoundingBox _abb; private Bitmap _bitmap; private int _count = 0; private int _max; private HttpContext _context; private Exception _ex; private int _cx = 900, _cy = 600; // Width and height of bitmap public void ProcessRequest (HttpContext context) { // Never called } public bool IsReusable { get { return false; } } public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object state) { _context = context; // Extract user input from the query string string city = context.Request["City"]; string region = context.Request["State"]; string scale = context.Request["Scale"]; // If city or state wasn’t specified, throw an exception if (String.IsNullOrEmpty(city) || String.IsNullOrEmpty(region)) throw new ArgumentException( "City and state must be specified via query string"); // Determine the scale if (!String.IsNullOrEmpty (scale)) { switch (scale) { case "1": _scale = Scale.Scale1m; break; case "2": _scale = Scale.Scale2m; break; case "4": _scale = Scale.Scale4m; break; case "8": _scale = Scale.Scale8m; break; case "16": _scale = Scale.Scale16m; break; case "32": _scale = Scale.Scale32m; break; } } // Instantiate a TerraServiceAsyncResult _ar = new TerraServiceAsyncResult(cb, state); // Instantiate the TerraService proxy _ts = new TerraService(); // Make an async call to get the latitude and longitude // of the requested city Place place = new Place(); place.City = city; place.State = region; place.Country = "USA"; _ts.BeginConvertPlaceToLonLatPt(place, new AsyncCallback(ConvertPlaceToLonLatCompleted), null); // Return an IAsyncResult that delays EndProcessRequest until // the final asynchronous Web service call has completed return _ar; } private void ConvertPlaceToLonLatCompleted(IAsyncResult ar) { try { // Complete the async call LonLatPt point = _ts.EndConvertPlaceToLonLatPt(ar); // Make an async call to compute the parameters // for a bounding box _ts.BeginGetAreaFromPt(point, Theme.Photo, _scale, _cx, _cy, new AsyncCallback(GetAreaFromPointCompleted), null); } catch (Exception ex) { _ex = ex; _ar.CompleteCall(); } } private void GetAreaFromPointCompleted(IAsyncResult ar) { try { // Complete the async call _abb = _ts.EndGetAreaFromPt(ar); // Create an image to fit the bounding box _bitmap = new Bitmap(_cx, _cy, PixelFormat.Format32bppRgb); int x1 = _abb.NorthWest.TileMeta.Id.X; int y1 = _abb.NorthWest.TileMeta.Id.Y; int x2 = _abb.NorthEast.TileMeta.Id.X; int y2 = _abb.SouthWest.TileMeta.Id.Y; _max = (x2 - x1 + 1) * (y1 - y2 + 1); // Place concurrent async calls to TerraService to // fetch image tiles for (int x = x1; x <= x2; x++) { for (int y = y1; y >= y2; y--) { TileId tid = new TileId(); tid.Theme = _abb.NorthWest.TileMeta.Id.Theme; tid.Scale = _abb.NorthWest.TileMeta.Id.Scale; tid.Scene = _abb.NorthWest.TileMeta.Id.Scene; tid.X = x; tid.Y = y; _ts.BeginGetTile(tid, new AsyncCallback(GetTileCompleted), new Point(x - x1, y1 - y)); } } } catch (Exception ex) { _ex = ex; _ar.CompleteCall(); } } private void GetTileCompleted(IAsyncResult ar) { try { // Complete the async call using(Image tile = Image.FromStream( new MemoryStream(_ts.EndGetTile(ar)))) { // Draw the tile onto the bitmap Point point = (Point)ar.AsyncState; int dx = point.X; int dy = point.Y; lock (_bitmap) { using(Graphics g = Graphics.FromImage(_bitmap)) { g.DrawImage(tile, dx * tile.Width – (int)_abb.NorthWest.Offset.XOffset, dy * tile.Height – (int)_abb.NorthWest.Offset.YOffset, tile.Width, tile.Height); } } // Increment the tile count and complete the request if all // tiles have been fetched int count = Interlocked.Increment(ref _count); if (count == _max) _ar.CompleteCall(); } catch (Exception ex) { _ex = ex; _ar.CompleteCall(); } } public void EndProcessRequest(IAsyncResult ar) { if (_ex != null) { // If an exception was thrown, rethrow it throw _ex; } else { // Otherwise return the generated image _context.Response.ContentType = "image/jpeg"; _bitmap.Save(_context.Response.OutputStream, ImageFormat.Jpeg); _bitmap.Dispose(); } } } class TerraServiceAsyncResult : IAsyncResult { private AsyncCallback _cb; private object _state; private ManualResetEvent _event; private bool _completed = false; private object _lock = new object(); public TerraServiceAsyncResult(AsyncCallback cb, object state) { _cb = cb; _state = state; } public Object AsyncState { get { return _state; } } public bool CompletedSynchronously { get { return false; } } public bool IsCompleted { get { return _completed; } } public WaitHandle AsyncWaitHandle { get { lock (_lock) { if (_event == null) _event = new ManualResetEvent(IsCompleted); return _event; } } } public void CompleteCall() { lock (_lock) { _completed = true; if (_event != null) _event.Set(); } if (_cb != null) _cb(this); } }
当 ASP.NET 调用处理程序的 BeginProcessRequest 方法时,开始异步处理。通过 TerraService 代理的 BeginConvertPlaceToLonLatPt 方法,BeginProcessRequest 可对 TerraService 进行异步调用。然后,分配给该请求的线程返回线程池中。异步调用完成时,另一个线程被从线程池中调出以执行 ConvertPlaceToLonLatCompleted 方法。该线程会检索上次调用的结果,进行自己的异步调用,然后返回线程池。这种模式不断重复直至所有异步调用完成,此时,调用处理程序的
EndProcessRequest 方法,产生的位图被返回给请求者。
要阻止 EndProcessRequest 直至最后的 Web 服务调用完成,AsyncTerraServiceImageGrabber 返回来自 BeginProcessRequest 的 IAsyncResult 的自我实现。如果它要返回由 BeginConvertPlaceToLonLatPt 返回的 IAsyncResult,则在第一个 Web 服务调用完成时,需调用 EndProcessRequest(并终止请求)。
实现 IAsyncResult 和 TerraServiceAsyncResult 的类具有可随时调用以完成请求的公共 CompleteCall 方法。通常,只有在最后的 Web 服务调用完成后,AsyncTerraServiceImageGrabber 才调用 CompleteCall。不过,如果在 BeginProcessRequest 和 EndProcessRequest 之间执行的某一方法抛出异常,处理程序将异常缓存在私有字段 (_ex) 中,调用 CompleteCall 以终止请求,然后从 EndProcessReques
中重新抛出异常。否则,异常将丢失,请求将无法完成。
由于 AsyncTerraServiceImageGrabber 使用 ASP.NET 线程的时间只是处理请求所需的总时间的一小部分,因此,AsyncTerraServiceImageGrabber 比其同步版的同类方法具有更高的可扩展性。大部分时间里,它只是等待异步 Web 服务调用完成。
理论上,AsyncTerraServiceImageGrabber 还胜过 TerraServiceImageGrabber,因为它不是顺序地重复调用 TerraService's GetTile 方法,而是并行调用。不过,实际上,每次只有两个针对给定 IP 地址的出站调用可以被挂起,除非您提高了运行库的默认 maxconnection 设置:
<system.net> <connectionManagement> <add address="*" maxconnection="20" /> </connectionManagement> </system.net>
其他配置设置也可影响并发。有关详细信息,请参考知识库文章“从 ASP.NET 应用程序进行 Web 服务请求时出现的争用、性能不佳和死锁等问题”(support.microsoft.com/kb/821268)。
即使每次只执行一个调用,但 AsyncTerraServiceImageGrabber 并不比 TerraServiceImageGrabber 差。它的设计非常出色,因为它尽可能有效地使用了 ASP.NET 线程。
异步 HTTP 模块
您在 ASP.NET 中可能利用的第三个异步编程模型是异步 HTTP 模块。HTTP 模块是位于 ASP.NET 管道中的对象,在管道中,它可以查看甚至修改传入请求和传出响应。ASP.NET 中的许多主要服务都是以 HTTP 模块的形式实现的,包括身份验证、授权和输出缓存。通过编写自定义 HTTP 模块并将它们插入管道,您可以扩展 ASP.NET。当您这样做的时候,一定要认真考虑这些 HTTP 模块是否应当是异步的。
图 6 包括称为 RequestLogModule 的简单、同步 HTTP 模块的源代码,它在名为 RequestLog.txt 的文本文件中记录了传入请求。在站点的 App_Data 目录下创建该文件,这样用户就无法浏览它。(要注意 ASP.NET 作为安全主体的运行(例如,ASPNET
或网络服务)必须写入对 App_Data 的使用权限。)该模块实现 IHttpModule 接口,这是 HTTP 模块的唯一要求。加载该模块时,其 Init 方法会为 HttpApplication.PreRequestHandlerExecute 事件注册一个处理程序,该程序从每个请求的管道中被触发。事件处理程序打开 RequestLog.txt(或在该文件不存在的情况下创建一个),然后将一行包含关于当前请求的有针对性的信息写入其中,包括请求到达的时间和日期、请求者的用户名(如果请求是要进行身份验证的,或者如果身份验证关闭,则要包含请求者的
IP 地址),以及请求的 URL。该模块在 web.config 的 <httpModules> 部分进行注册,以便在每次应用程序启动时,提示 ASP.NET 加载该文件。
Figure 6 同步
HTTP 模块
using System; using System.Web; using System.IO; public class RequestLogModule : IHttpModule { public void Init (HttpApplication application) { application.PreRequestHandlerExecute += new EventHandler(OnPreRequestHandlerExecute); } public void Dispose () {} void OnPreRequestHandlerExecute(Object sender, EventArgs e) { HttpApplication app = (HttpApplication)sender; DateTime time = DateTime.Now; using(StreamWriter writer = new StreamWriter( app.Server.MapPath("~/App_Data/RequestLog.txt"), true)) { string line = String.Format( "{0,10:d} {1,11:T} {2, 32} {3}", time, time, app.User.Identity.IsAuthenticated ? app.User.Identity.Name : app.Request.UserHostAddress, app.Request.Url); writer.WriteLine (line); } } }
RequestLogModule 存在两方面的问题。首先,每次请求时均要执行 I/O 文件。其次,它使用请求处理线程来执行 I/O,否则,线程可能被用于为其他传入请求服务。由于简单,该模块会导致吞吐量损失。通过批处理 I/O 文件操作,您可能会缓解延迟,更好的方法是使模块异步(或者最好批处理 I/O 文件并使模块异步)。
图 7 显示了异步版本的 RequestLogModule。调用 AsyncRequestLogModule 后,它将执行完全相同的工作,并将分配给请求的线程返回线程池,然后写入文件。当写入完成时,从线程池中调出新的线程,用于完成请求。
Figure 7 异步
HTTP 模块
using System; using System.Web; using System.IO; using System.Threading; using System.Text; public class AsyncRequestLogModule : IHttpModule { private FileStream _file; private static long _position = 0; private static object _lock = new object(); public void Init (HttpApplication application) { application.AddOnPreRequestHandlerExecuteAsync ( new BeginEventHandler (BeginPreRequestHandlerExecute), new EndEventHandler (EndPreRequestHandlerExecute) ); } IAsyncResult BeginPreRequestHandlerExecute (Object source, EventArgs e, AsyncCallback cb, Object state) { HttpApplication app = (HttpApplication)source; DateTime time = DateTime.Now; string line = String.Format( "{0,10:d} {1,11:T} {2, 32} {3}\r\n", time, time, app.User.Identity.IsAuthenticated ? app.User.Identity.Name : app.Request.UserHostAddress, app.Request.Url); byte[] output = Encoding.ASCII.GetBytes(line); lock (_lock) { _file = new FileStream( HttpContext.Current.Server.MapPath( "~/App_Data/RequestLog.txt"), FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write, 1024, true); _file.Seek(_position, SeekOrigin.Begin); _position += output.Length; return _file.BeginWrite(output, 0, output.Length, cb, state); } } void EndPreRequestHandlerExecute (IAsyncResult ar) { _file.EndWrite(ar); _file.Close(); } public void Dispose () {} }
如何使 AsyncRequestLogModule 异步?其 Init 方法调用 HttpApplication.AddOnPreRequestHandlerExecuteAsync 以便为 PreRequestHandlerExecute 事件注册 Begin 和 End 方法。HttpApplication 类包含针对其他 per-request 事件的其他 AddOn 方法。例如,HTTP 模块可以调用 AddOnBeginRequestAsync 以便为 BeginRequest 事件注册异步处理程序。AsyncRequestLogModule
的 BeginPreRequestHandlerExecute 方法使用 Framework 的 FileStream.BeginWrite 方法来开始异步写入。BeginPreRequestHandlerExecute 返回时,线程返回线程池。
AsyncRequestLogModule 包含一些值得特别一提的线程同步逻辑。运行在多个线程中的多个请求可能要同时写入日志文件。为了确保并发写入不会相互覆盖,AsyncRequestLogModule 在由所有模块实例共享的私有字段中保存了下一个写入在文件中的位置 (_position)。每次调用 BeginWrite 之前,模块从字段中读取该位置并更新字段以指向要写入该文件的内容的第一个字节。读取并更新 _position 的逻辑包含在 lock 语句中,这样每次就有不止一个线程可执行它。这防止了在一个线程有机会更新位置之前,另一个线程读取该位置。
总结
异步编程是尽可能高效地使用 ASP.NET 线程池来构建扩展性更强的应用程序的一种很好的方法。以往,我很少看到 ASP.NET 开发人员使用异步编程模型,部分原因在于他们并不知道存在这些模型。不要让稀疏文档成为您的“拦路虎”,从现在起就开始异步思考,今后您将会构建出更好的应用程序。
请注意,本文提供了 C# 和 Visual Basic® 版本的可下载示例代码。我常常收到要求提供 Visual
Basic 版示例的电子邮件。这一次,您不必再问了,我已经提供了该版本的示例!
相关文章推荐
- 通过 ASP.NET 异步编程实现可扩展的应用程序
- 通过 ASP.NET 异步编程实现可扩展的应用程序
- 通过 ASP.NET 异步编程实现可扩展的应用程序
- 通过扩展改善ASP.NET MVC的验证机制[实现篇]
- [Asp.net Mvc]通过UrlHelper扩展实现js,css修改后重新加载
- 通过扩展改善ASP.NET MVC的验证机制[实现篇]
- 通过扩展改善ASP.NET MVC的验证机制[实现篇]
- ASP.NET文件下载简单实现(也可以通过直接读取数据库 大字段文件,如oracle 中的bolg,long raw 等大字段文件)
- ASP.NET 应用程序的扩展策略[MSDN 杂志]
- ASP.NET jQuery 食谱24 (通过AJAX简单实现DropDownList二级联动)
- ASP.NET jQuery 实例9 通过控件hyperlink实现返回顶部效果
- 讨论asp.net通过机器cookie仿百度(google)实现搜索input搜索提示弹出框自己主动
- ASP.NET通过自定义函数实现对字符串的大小写切换功能
- 在ASP.NET MVC中使用Knockout实践02,组合View Model成员、Select绑定、通过构造器创建View Model,扩展View Model方法
- 使用HttpModules实现Asp.net离线应用程序
- ASP.NET MVC 通过ActionFilter 实现AOP设计 示例
- ASP.NET控件使用——Repeater通过CommandName实现删除功能
- 浅谈asp.net通过本机cookie仿百度(google)实现搜索input框自己主动弹出搜索提示
- 如何使用 C# .NET 在 ASP.NET 应用程序中实现基于窗体的身份验证
- Asp.Net MVC3 简单入门第一季(五) 通过Asp.Net MVC的区域功能实现将多个MVC项目部署到一个站点