asp.net

位置:IT落伍者 >> asp.net >> 浏览文章

ASP.NET 2.0异步页面原理浅析


发布日期:2019年05月19日
 
ASP.NET 2.0异步页面原理浅析

与 ASPNET 相比ASPNET 的各方面改进可以说是非常巨大的但就其实现层面来说最大的增强莫过于提供了对异步 页面的支持通过此机制编写良好的页面可以将数据库WebService 调用等慢速操作对网站吞吐能力的影响降到最低并极大的改善网站的平均页 面响应速度本文将从使用和实现两个层面简单的剖析这一强大机制的原理以便读者能够更好的应用这一机制

对一个网页请求的生命周 期来说首先是 Web 服务器收到客户端 HTTP 请求将请求转交给 ASPNET 引擎引擎将以流水线方式调用合适的 Web 应用程序和最 终的页面进行处理页面会根据请求内容执行某些后台操作如访问数据库调用远程 WebService 等等最终将结果以某种可视化形式展示到最 终用户的浏览器中

而为了提高响应速度和吞吐量现代的 Web 服务器往往会将 Web 应用程序和页面放在一个缓沖池中备用 避免每次处理请求时重建环境而请求到来时Web 服务器会从一个系统线程池中获取临时线程调用从 Web 应用程序和页面缓沖池中获取的处理实例 完成对请求的处理并最终返回处理的结果

咋一看这种机制非常完美能够最大限度的重用系统资源但实际上其中存在着很大的优化余 地

我们可以将一个页面请求的处理过程进一步细化为下面步骤

Web 服务器接受请求并由 引擎转发请求

页面处理请求访问数据库调用远程 WebService 等

页面将处理结果以某种形 式展现如 HTML 表格等

Web 服务器将结果返回给最终客户的浏览器

其中第一步涉及到核心态 网络驱动需要频繁切换回用户态以将请求转交给处理引擎这里涉及到大量的核心态和用户态切换为减少这个负担IIS 开始提供 了 httpsys 在核心态直接对大多数请求进行处理这是 MS 在中间件一级就已经替我们做好的优化我们无需也无法关心

而 另外一个潜在的优化点就是异步页面的目标增强页面处理请求的并发性

Web 服务器在从线程池获取临时线程后在线程中调用页面相 关代码处理请求而这里的请求处理过程往往涉及到较为缓慢的操作例如访问数据库调用远程 WebService 等如果数据库是在本机的话还好系 统 CPU 时间只是从处理线程移交到后台数据库线程而一旦处理运算逻辑在远程例如访问外部独立数据库或调用 WebService 完成某种操 作此时此线程就只能无谓的等待操作结束而作为 Web 服务器处理客户端请求的线程池其最大容纳线程的数量肯定是有限的(虽然大多数情况下这个上 限值可以修改例如 ASPNET 中可以通过修改 nfig 的 processModel 标签调整最大数量缺省一 旦超过此数量的请求正在并行执行或者说正在等待后台慢速的操作此时新来的请求就会因为处理请求线程池中无可用线程出现虽然 CPU 负荷非常低但 仍出现 服务器不可用 类似的错误从而事实上造成对应用的 DoS(拒绝服务)攻击即使此上限设置很大也会因为大量等待操作降低其它 本可以快速的页面的响应速度

要处理这种情况虽然可以通过继续增大请求处理线程池最大容量缓解但总是治标不治本更好的 方法就是将请求处理和页面处理分离避免慢速页面处理占用快速请求处理的时间页面在接受到引擎的处理页面请求后通过调用异步方法来试图完成实际页面处 理处理结果从单独线程池获取线程进行监控而发送页面请求的请求处理线程将被直接释放以便继续处理其它的页面请求这也就是 ASPNET 异步页 面的基本思路实际上这个思路在 ASPNET 时就已经提出Fritz Onion 曾于 年在 MSDN 杂志发表过一篇文章 详细讨论这个问题并给出了一个简单的解决方案

Use Threads and Build Asynchronous Handlers in Your ServerSide Web Code

文 中提供的实现很好的对此问题进行原理上的验证但从实现角度较为繁琐需要自行处理 IAsyncResult 接口以及自定义线程池而且缺少 对 HTTP 上下文以及超时等的处理

好在 ASPNET 对此问题提供了内建的支 持Jeff Prosise 在 MSDN 杂志的文章中详细的讨论了其实现思路和使用方法

Asynchronous Pages in ASPNET

从使用角 度来说异步页面的支持非常透明使用者只需要在页面定义的 Page 标签中指定异步模式例如

<%@ Page Async=true %>

然 后就可以在 Page 的实现代码中通过 Page 类型 的 AddOnPreRenderCompleteAsync 或 PageAsyncTask 方法提交异步的页面处理代码ASPNET 引擎会 根据页面的异步模式设定调用合适的页面处理开始和结束方法

对大多数简单的异步处理情况可以直接调 用 AddOnPreRenderCompleteAsync 方法提交页面请求开始和结束时的处理代码例如上述文章中给出的一个内部处 理 HTTP 页面请求的例子

// AsyncPageaspxcs

using System;

using SystemWeb;

using SystemWebUI;

using SystemWebUIWebControls;

using SystemNet;

using SystemIO;

using SystemText;

using SystemTextRegularExpressions;

public partial class AsyncPage : SystemWebUIPage

{

private WebRequest _request;

void Page_Load (object sender EventArgs e)

{

AddOnPreRenderCompleteAsync (

new BeginEventHandler(BeginAsyncOperation)

new EndEventHandler (EndAsyncOperation)

);

}

IAsyncResult BeginAsyncOperation (object sender EventArgs e

AsyncCallback cb object state)

{

_request = WebRequestCreate();

return _requestBeginGetResponse (cb state);

}

void EndAsyncOperation (IAsyncResult ar)

{

string text;

using (WebResponse response = _requestEndGetResponse(ar))

{

using (StreamReader reader =

new StreamReader(responseGetResponseStream()))

{

text = readerReadToEnd();

}

}

Regex regex = new Regex (href\\s*=\\s*\([^\]*)\

RegexOptionsIgnoreCase);

MatchCollection matches = regexMatches(text);

StringBuilder builder = new StringBuilder();

foreach (Match match in matches)

{

builderAppend (matchGroups[]);

builderAppend(<br/>);

}

OutputText = builderToString ();

}

}

AsyncPage 页面的 OnLoad 事件中 提交异步处理方法ASPNET 引擎会在页面加载完成后调用 BeginAsyncOperation 方法启动异步方法这里的异步请求是打开一 个远程 Web 页面而大多数诸如数据库WebService 调用等等都提供了类似的异步调用版本页面处理开始方法会返回异步调用请求 的 IAsyncResult 封装通过此接口检测处理的完成情况而在 BeginAsyncOperation 方法返回之后处理连接请求的线程 将回到线程池用来处理后续的连接请求直到实际的异步处理操作完成例如 Web 页面被取回引擎才会从独立线程池中获取临时线程调 用 EndAsyncOperation 方法完成后续的操作

而 PageAsyncTask 的方式则是增强版本除了异步页面处理开始和结束方法自 身外还可以提供在超时情况下的处理方法以及处理时的状态对象上述文章中给出的对应例子如下

// AsyncPageTaskaspxcs

using System;

using SystemWeb;

using SystemWebUI;

using SystemWebUIWebControls;

using SystemNet;

using SystemIO;

using SystemText;

using SystemTextRegularExpressions;

public partial class AsyncPageTask : SystemWebUIPage

{

private WebRequest _request;

protected void Page_Load(object sender EventArgs e)

{

PageAsyncTask task = new PageAsyncTask(

new BeginEventHandler(BeginAsyncOperation)

new EndEventHandler(EndAsyncOperation)

new EndEventHandler(TimeoutAsyncOperation)

null

);

RegisterAsyncTask(task);

}

IAsyncResult BeginAsyncOperation(object sender EventArgs e

AsyncCallback cb object state)

{

_request = WebRequestCreate();

return _requestBeginGetResponse(cb state);

}

void EndAsyncOperation(IAsyncResult ar)

{

string text;

using (WebResponse response = _requestEndGetResponse(ar))

{

using (StreamReader reader =

new StreamReader(responseGetResponseStream()))

{

text = readerReadToEnd();

}

}

Regex regex = new Regex(href\\s*=\\s*\([^\]*)\

RegexOptionsIgnoreCase);

MatchCollection matches = regexMatches(text);

StringBuilder builder = new StringBuilder();

foreach (Match match in matches)

{

builderAppend(matchGroups[]);

builderAppend(<br/>);

}

OutputText = builderToString();

}

void TimeoutAsyncOperation(IAsyncResult ar)

{

OutputText = Data temporarily unavailable;

}

}

为验证这一机制的实现效果我们可以在各个 方法的入口处设置断点因为 VS 的 IDE 屏蔽了底层 CLR 实现信息我们需要在 Debug\Windows \Immediate Window 窗口中输入 load sos 命令加载 CLR 调试支持具体的 sos 命令可以输入 !help 查询 帮助或参考我以前《用WinDbg探索CLR世界》的系列文章这里不再罗嗦

在 AsyncPage 类型 的 Page_LoadBeginAsyncOperation 和 EndAsyncOperation 方法中分别输入 !ClrStack 命 令可以获取当前线程的调用堆栈

!clrstack

OS Thread Id: xb ()

ESP EIP

dd ac AsyncPagePage_Load(SystemObject SystemEventArgs)

df dd SystemWebHttpRuntimeProcessRequest(SystemWebHttpWorkerRequest)

!clrstack

OS Thread Id: xb ()

ESP EIP

dc ad AsyncPageBeginAsyncOperation(SystemObject SystemEventArgs SystemAsyncCallback SystemObject)

df dd SystemWebHttpRuntimeProcessRequest(SystemWebHttpWorkerRequest)

!clrstack

OS Thread Id: xd ()

ESP EIP

ceee feff AsyncPageEndAsyncOperation(SystemIAsyncResult)

cfc fec SystemNetConnectionReadComplete(Int SystemNetWebExceptionStatus)

可以看到 Page_Load 和 BeginAsyncOperation 方法都是在 ID 为 的线程中被调用其调用源也都是处 理 HTTP 请求的 HttpRuntimeProcessRequest 方法而 EndAsyncOperation 则是在另外一 个 ID 为 的线程中调用调用源也是完成网络读操作的 ConnectionReadComplete 方法

而从实现角度来看AddOnPreRenderCompleteAsync 方法将异步页面处理的启动和停止方法放到一 个 PagePageAsyncInfo 对象中此对象维护了与页面相关的各种上下文信息以及开始停止和状态的数组 而 RegisterAsyncTask 方法也是类似将 PageAsyncTask 实例放到 PageAsyncTaskManager 类型的 管理器中

class Page

{

private PagePageAsyncInfo _asyncInfo;

private PageAsyncTaskManager _asyncTaskManager;

public void AddOnPreRenderCompleteAsync(BeginEventHandler beginHandler EndEventHandler endHandler object state)

{

// 处理参数和状态异常情况

// 延迟构造异步页面信息

if (_asyncInfo == null)

_asyncInfo = new PagePageAsyncInfo(this);

_asyncInfoAddHandler(beginHandler endHandler state);

}

public void RegisterAsyncTask(PageAsyncTask task)

{

// 处理参数和状态异常情况

// 延迟构造异步任务管理器

if (this_asyncTaskManager == null)

_asyncTaskManager = new PageAsyncTaskManager(this);

_asyncTaskManagerAddTask(task);

}

}

HttpApplication 在处理页面请求 时通过其 pipeline 的 CallHandlerExecutionStep 步骤调用页面的 BeginProcessRequest 方 法其伪代码如下

void HttpApplicationIExecutionStepExecute()

{

// 从上下文中获取获取当前页面的处理器

HttpContext context = _applicationContext;

IHttpHandler handler = contextHandler;

if (handler == null)

{

_sync = true;

}

else if (handler is IHttpAsyncHandler)

{

// 如果是异步处理器则调用异步处理开始方法

IHttpAsyncHandler asyncHandler = (IHttpAsyncHandler) handler;

_sync = false;

_handler = asyncHandler;

IAsyncResult result = asyncHandlerBeginProcessRequest(context _completionCallback null);

// 如果的确是异步操作就直接返回

if (!resultCompletedSynchronously)

return;

// 否则恢复同步的页面处理流程

_sync = true;

_handler = null;

asyncHandlerEndProcessRequest(result);

}

else

{

// 采用同步模式处理页面

_sync = true;

_applicationSyncContextSetSyncCaller();

try

{

handlerProcessRequest(context);

}

finally

{

_applicationSyncContextResetSyncCaller();

}

}

}

而 ASPNET 页面一旦通过 Page 标记 定义为异步模式其编译生成的 Page 子类就会实现 IHttpAsyncHandler 接口

例如对上述例子我们可以通 过 !ClrStack 命令看到页面被编译为名称为 ASPasyncpage_aspx 的类型

!clrstack

OS Thread Id: xb ()

ESP EIP

dd ac AsyncPagePage_Load(SystemObject SystemEventArgs)

ddc be SystemWebUIPageAsyncPageBeginProcessRequest(SystemWebHttpContext SystemAsyncCallback SystemObject)

dd b ASPasyncpage_aspxBeginProcessRequest(SystemWebHttpContext SystemAsyncCallback SystemObject)

ddec bb SystemWebHttpApplication+CallHandlerExecutionStepSystemWebHttpApplicationIExecutionStepExecute()

de c SystemWebHttpApplicationExecuteStep(IExecutionStep Boolean ByRef)

进 一步使用 !DumpDomain 命令可以找到其页面编译的临时文件

!DumpDomain

Assembly: ade [D:\WINDOWS\MicrosoftNET\Framework\v\Temporary ASPNET Files\asyncpage\baa\cdd\App_Web_ngemvdll]

ClassLoader: abc

SecurityDescriptor: ac

Module Name

acc D:\WINDOWS\MicrosoftNET\Framework\v\Temporary ASPNET Files\asyncpage\baa\cdd\App_Web_ngemvdll

使 用 IL 反汇编根据打开此文件可以看到 AsyncPageaspx 被编译为 asyncpage_aspx 类型如下所示

public class asyncpage_aspx : AsyncPage IHttpAsyncHandler IHttpHandler

{

public virtual IAsyncResult BeginProcessRequest(HttpContext context AsyncCallback cb object data)

{

return baseAsyncPageBeginProcessRequest(context cb data);

}

public virtual void EndProcessRequest(IAsyncResult ar)

{

baseAsyncPageEndProcessRequest(ar);

}

}

public interface IHttpAsyncHandler : IHttpHandler

{

// Methods

IAsyncResult BeginProcessRequest(HttpContext context AsyncCallback cb object extraData);

void EndProcessRequest(IAsyncResult result);

}

其中 AsyncPage 类型是后台实现代码编译 生成的类型asyncpage_aspx 则是 aspx 页面编译生成

而 在 PageAsyncPageBeginProcessRequest 方法中将首先处理上下文环境初始化初始化异步执行信息以及相应回调函数 的执行然后会调 用 PageAsyncTaskManagerRegisterHandlersForPagePreRenderCompleteAsync 将异步 任务管理器中所有的异步任务封装后注册到 PagePageAsyncInfo 对象中维护的异步调用信息中最后调 用 其 CallHandlers 方法完成对异步处理开始方法的调用完整的伪代码如下

class Page

{

protected IAsyncResult AsyncPageBeginProcessRequest(HttpContext context AsyncCallback callback object extraData)

{

// 处理上下文环境初始化

// 初始化异步执行信息

_asyncInfoAsyncResult = new HttpAsyncResult(callback extraData);

_asyncInfoCallerIsBlocking = callback == null;

// 执行相应回调函数

// 注册异步任务

if ((_asyncTaskManager != null) && !_asyncInfoCallerIsBlocking)

_asyncTaskManagerRegisterHandlersForPagePreRenderCompleteAsync();

// 调用所有的异步处理开始方法

_asyncInfoCallHandlers(true);

return _asyncInfoAsyncResult;

}

}

而 在 PageAsyncTaskManager 中被管理的异步任务会作为一个异步执行信息注册到 PageAsyncInfo 中去并在其被调用 时实际调用 PageAsyncTaskManager 类型的 ExecuteTasks 方法实现较为复杂的异步调用逻辑

internal class PageAsyncTaskManager

{

internal void RegisterHandlersForPagePreRenderCompleteAsync()

{

_pageAddOnPreRenderCompleteAsync(new BeginEventHandler(thisBeginExecuteAsyncTasks) new EndEventHandler(thisEndExecuteAsyncTasks));

}

private IAsyncResult BeginExecuteAsyncTasks(object sender EventArgs e AsyncCallback cb object extraData)

{

return ExecuteTasks(cb extraData);

}

private void EndExecuteAsyncTasks(IAsyncResult ar)

{

_asyncResultEnd();

}

}

以上我们对异步页面的目的范围使用方式 和实现原理等有了一个大致的了解并针对异步任务的管理做了简要的分析基本上已经能弄清异步页面的静态运行机制如何下一节我们将从动态执行的角度 对两级异步任务以及相应的调度和线程使用做进一步探索

               

上一篇:在Asp.net(C#)中添加程序实现验证码功能

下一篇:ASP.NET入门随想之抽象的力量