zoukankan      html  css  js  c++  java
  • ASP.NET 2.0 异步页面原理浅析 [1]

     
          与 ASP.NET 1.0 相比,ASP.NET 2.0 的各方面改进可以说是非常巨大的。但就其实现层面来说,最大的增强莫过于提供了对异步页面的支持。通过此机制,编写良好的页面可以将数据库、WebService 调用等慢速操作,对网站吞吐能力的影响降到最低,并极大的改善网站的平均页面响应速度。本文将从使用和实现两个层面,简单的剖析这一强大机制的原理,以便读者能够更好的应用这一机制。
          对一个网页请求的生命周期来说,首先是 Web 服务器收到客户端 HTTP 请求,将请求转交给 ASP.NET 引擎;引擎将以流水线方式调用合适的 Web 应用程序和最终的页面进行处理;页面会根据请求内容,执行某些后台操作,如访问数据库、调用远程 WebService 等等;最终将结果以某种可视化形式,展示到最终用户的浏览器中。
          而为了提高响应速度和吞吐量,现代的 Web 服务器往往会将 Web 应用程序和页面,放在一个缓冲池中备用,避免每次处理请求时重建环境。而请求到来时,Web 服务器会从一个系统线程池中获取临时线程,调用从 Web 应用程序和页面缓冲池中获取的处理实例,完成对请求的处理,并最终返回处理的结果。
          咋一看这种机制非常完美,能够最大限度的重用系统资源,但实际上其中存在着很大的优化余地。

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

          1.Web 服务器接受请求并由引擎转发请求
          2.页面处理请求,访问数据库、调用远程 WebService 等
          3.页面将处理结果,以某种形式展现,如 HTML 表格等
          4.Web 服务器将结果返回给最终客户的浏览器

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

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

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

          Use Threads and Build Asynchronous Handlers in Your Server-Side Web Code

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

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

          Asynchronous Pages in ASP.NET 2.0

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

    <%@ Page Async="true" ... %>


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

          对大多数简单的异步处理情况,可以直接调用 AddOnPreRenderCompleteAsync 方法,提交页面请求开始和结束时的处理代码,例如上述文章中给出的一个内部处理 HTTP 页面请求的例子:
     1// AsyncPage.aspx.cs
     2using System;
     3using System.Web;
     4using System.Web.UI;
     5using System.Web.UI.WebControls;
     6using System.Net;
     7using System.IO;
     8using System.Text;
     9using System.Text.RegularExpressions;
    10
    11public partial class AsyncPage : System.Web.UI.Page
    12{
    13    private WebRequest _request;
    14
    15    void Page_Load (object sender, EventArgs e)
    16    {
    17        AddOnPreRenderCompleteAsync (
    18            new BeginEventHandler(BeginAsyncOperation),
    19            new EndEventHandler (EndAsyncOperation)
    20        );
    21    }

    22
    23    IAsyncResult BeginAsyncOperation (object sender, EventArgs e, 
    24        AsyncCallback cb, object state)
    25    {
    26        _request = WebRequest.Create("http://msdn.microsoft.com");
    27        return _request.BeginGetResponse (cb, state);
    28    }

    29    void EndAsyncOperation (IAsyncResult ar)
    30    {
    31        string text;
    32        using (WebResponse response = _request.EndGetResponse(ar))
    33        {
    34            using (StreamReader reader = 
    35                new StreamReader(response.GetResponseStream()))
    36            {
    37                text = reader.ReadToEnd();
    38            }

    39        }

    40
    41        Regex regex = new Regex ("href\s*=\s*"([^"]*)""
    42            RegexOptions.IgnoreCase);
    43        MatchCollection matches = regex.Matches(text);
    44
    45        StringBuilder builder = new StringBuilder(1024);
    46        foreach (Match match in matches)
    47        {
    48            builder.Append (match.Groups[1]);
    49            builder.Append("<br/>");
    50        }

    51
    52        Output.Text = builder.ToString ();
    53    }

    54}


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

     

          而 PageAsyncTask 的方式则是增强版本,除了异步页面处理开始和结束方法自身外,还可以提供在超时情况下的处理方法,以及处理时的状态对象。上述文章中给出的对应例子如下:
     
     1// AsyncPageTask.aspx.cs
     2using System;
     3using System.Web;
     4using System.Web.UI;
     5using System.Web.UI.WebControls;
     6using System.Net;
     7using System.IO;
     8using System.Text;
     9using System.Text.RegularExpressions;
    10
    11public partial class AsyncPageTask : System.Web.UI.Page
    12{
    13    private WebRequest _request;
    14
    15    protected void Page_Load(object sender, EventArgs e)
    16    {
    17        PageAsyncTask task = new PageAsyncTask(
    18            new BeginEventHandler(BeginAsyncOperation),
    19            new EndEventHandler(EndAsyncOperation),
    20            new EndEventHandler(TimeoutAsyncOperation),
    21            null
    22        );
    23        RegisterAsyncTask(task);
    24    }

    25
    26    IAsyncResult BeginAsyncOperation(object sender, EventArgs e, 
    27        AsyncCallback cb, object state)
    28    {
    29        _request = WebRequest.Create("http://msdn.microsoft.com");
    30        return _request.BeginGetResponse(cb, state);
    31    }

    32
    33    void EndAsyncOperation(IAsyncResult ar)
    34    {
    35        string text;
    36        using (WebResponse response = _request.EndGetResponse(ar))
    37        {
    38            using (StreamReader reader = 
    39                new StreamReader(response.GetResponseStream()))
    40            {
    41                text = reader.ReadToEnd();
    42            }

    43        }

    44
    45        Regex regex = new Regex("href\s*=\s*"([^"]*)""
    46            RegexOptions.IgnoreCase);
    47        MatchCollection matches = regex.Matches(text);
    48
    49        StringBuilder builder = new StringBuilder(1024);
    50        foreach (Match match in matches)
    51        {
    52            builder.Append(match.Groups[1]);
    53            builder.Append("<br/>");
    54        }

    55
    56        Output.Text = builder.ToString();
    57    }

    58
    59    void TimeoutAsyncOperation(IAsyncResult ar)
    60    {
    61        Output.Text = "Data temporarily unavailable";
    62    }

    63}



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

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

    !clrstack
    OS Thread Id: 0xb00 (2816)
    ESP       EIP     
    0361d9d4 05a9c817 AsyncPage.Page_Load(System.Object, System.EventArgs)
    ...
    0361df34 050dd403 System.Web.HttpRuntime.ProcessRequest(System.Web.HttpWorkerRequest)
    ...

    !clrstack
    OS Thread Id: 0xb00 (2816)
    ESP       EIP     
    0361dc04 05a9d819 AsyncPage.BeginAsyncOperation(System.Object, System.EventArgs, System.AsyncCallback, System.Object)
    ...
    0361df34 050dd403 System.Web.HttpRuntime.ProcessRequest(System.Web.HttpWorkerRequest)
    ...

    !clrstack
    OS Thread Id: 0xd30 (3376)
    ESP       EIP     
    04c5eee0 05fef30f AsyncPage.EndAsyncOperation(System.IAsyncResult)
    ...
    04c5f60c 05fe976c System.Net.Connection.ReadComplete(Int32, System.Net.WebExceptionStatus)
    ...


          可以看到 Page_Load 和 BeginAsyncOperation 方法都是在 ID 为 2816 的线程中被调用,其调用源也都是处理 HTTP 请求的 HttpRuntime.ProcessRequest 方法;而 EndAsyncOperation 则是在另外一个 ID 为 3376 的线程中调用,调用源也是完成网络读操作的 Connection.ReadComplete 方法。

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

     
     1class Page
     2{
     3    private Page.PageAsyncInfo _asyncInfo;
     4    private PageAsyncTaskManager _asyncTaskManager;
     5
     6  public void AddOnPreRenderCompleteAsync(BeginEventHandler beginHandler, EndEventHandler endHandler, object state)
     7  {
     8      // 处理参数和状态异常情况
     9      
    10      // 延迟构造异步页面信息
    11      if (_asyncInfo == null)      
    12        _asyncInfo = new Page.PageAsyncInfo(this);  
    13      
    14      _asyncInfo.AddHandler(beginHandler, endHandler, state);
    15    }

    16    
    17    public void RegisterAsyncTask(PageAsyncTask task)
    18    {
    19      // 处理参数和状态异常情况
    20    &nbnbsp; 
    21      // 延迟构造异步任务管理器
    22    if (this._asyncTaskManager == null)
    23      _asyncTaskManager = new PageAsyncTaskManager(this);
    24     
    25    _asyncTaskManager.AddTask(task);
    26    }

    27}


          HttpApplication 在处理页面请求时,通过其 pipeline 的 CallHandlerExecutionStep 步骤,调用页面的 BeginProcessRequest 方法,其伪代码如下:
     
     1void HttpApplication.IExecutionStep.Execute()
     2{
     3  // 从上下文中获取获取当前页面的处理器
     4  HttpContext context = _application.Context;
     5  IHttpHandler handler = context.Handler;
     6    
     7  if (handler == null)
     8  {
     9    _sync = true;
    10  }

    11  else if (handler is IHttpAsyncHandler)
    12  {
    13    // 如果是异步处理器,则调用异步处理开始方法
    14        IHttpAsyncHandler asyncHandler = (IHttpAsyncHandler) handler1;
    15    
    16    _sync = false;
    17    _handler = asyncHandler;
    18    
    19    IAsyncResult result = asyncHandler.BeginProcessRequest(context, _completionCallback, null);
    20            
    21        // 如果的确是异步操作,就直接返回            
    22        if (!result.CompletedSynchronously)
    23            return;
    24      
    25    // 否则恢复同步的页面处理流程
    26    _sync = true;
    27    _handler = null;
    28    asyncHandler.EndProcessRequest(result);
    29  }

    30  else
    31  {
    32      // 采用同步模式处理页面
    33    _sync = true;
    34    
    35    _application.SyncContext.SetSyncCaller();
    36    try
    37    {
    38      handler.ProcessRequest(context);
    39    }

    40    finally
    41    {
    42      _application.SyncContext.ResetSyncCaller();
    43    }

    44  }

    45}


          而 ASP.NET 页面一旦通过 Page 标记定义为异步模式,其编译生成的 Page 子类就会实现 IHttpAsyncHandler 接口。
          例如对上述例子,我们可以通过 !ClrStack 命令看到页面被编译为名称为 ASP.asyncpage_aspx 的类型。

    !clrstack
    OS Thread Id: 0xb00 (2816)
    ESP       EIP     
    0361d9d4 05a9c817 AsyncPage.Page_Load(System.Object, System.EventArgs)
    ...
    0361dd5c 0545b90e System.Web.UI.Page.AsyncPageBeginProcessRequest(System.Web.HttpContext, System.AsyncCallback, System.Object)
    0361dd98 0545b813 ASP.asyncpage_aspx.BeginProcessRequest(System.Web.HttpContext, System.AsyncCallback, System.Object)
    0361ddec 0545b6b6 System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
    0361de28 05400c18 System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef)
    ...


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

    !DumpDomain 

    ...

    Assembly: 0023ade0 [D:WINDOWSMicrosoft.NETFrameworkv2.0.50727Temporary ASP.NET Filesasyncpagea0a38653cd7d92App_Web_n97gem4v.dll]
    ClassLoader: 0023abc0
    SecurityDescriptor: 00229ac0
      Module Name
    05a502cc D:WINDOWSMicrosoft.NETFrameworkv2.0.50727Temporary ASP.NET Filesasyncpagea0a38653cd7d92App_Web_n97gem4v.dll

    ...


          使用 IL 反汇编根据打开此文件,可以看到 AsyncPage.aspx 被编译为 asyncpage_aspx 类型,如下所示:
     
     1public class asyncpage_aspx : AsyncPage, IHttpAsyncHandler, IHttpHandler
     2{
     3    public virtual IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object data)
     4    {
     5    return base.AsyncPageBeginProcessRequest(context, cb, data);
     6    }

     7 
     8    public virtual void EndProcessRequest(IAsyncResult ar)
     9    {    
    10    base.AsyncPageEndProcessRequest(ar);
    11    }

    12}

    13
    14public interface IHttpAsyncHandler : IHttpHandler
    15{
    16  // Methods
    17  IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData);
    18  void EndProcessRequest(IAsyncResult result);
    19}


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

          而在 Page.AsyncPageBeginProcessRequest 方法中,将首先处理上下文环境初始化,初始化异步执行信息,以及相应回调函数的执行;然后会调用 PageAsyncTaskManager.RegisterHandlersForPagePreRenderCompleteAsync 将异步任务管理器中所有的异步任务,封装后注册到 Page.PageAsyncInfo 对象中维护的异步调用信息中;最后调用      其 CallHandlers 方法完成对异步处理开始方法的调用。完整的伪代码如下:
     
     1class Page
     2{
     3  protected IAsyncResult AsyncPageBeginProcessRequest(HttpContext context, AsyncCallback callback, object extraData)
     4    {
     5        // 处理上下文环境初始化
     6        
     7        // 初始化异步执行信息
     8        _asyncInfo.AsyncResult = new HttpAsyncResult(callback, extraData);
     9    _asyncInfo.CallerIsBlocking = callback == null;
    10
    11        // 执行相应回调函数
    12        
    13        // 注册异步任务
    14        if ((_asyncTaskManager != null&& !_asyncInfo.CallerIsBlocking)    
    15      _asyncTaskManager.RegisterHandlersForPagePreRenderCompleteAsync();
    16    
    17    // 调用所有的异步处理开始方法
    18        _asyncInfo.CallHandlers(true);
    19    
    20        return _asyncInfo.AsyncResult;    
    21    }

    22}


          而在 PageAsyncTaskManager 中被管理的异步任务,会作为一个异步执行信息注册到 PageAsyncInfo 中去。并在其被调用时,实际调用 PageAsyncTaskManager 类型的 ExecuteTasks 方法,实现较为复杂的异步调用逻辑。
     
     1internal class PageAsyncTaskManager
     2{
     3    internal void RegisterHandlersForPagePreRenderCompleteAsync()
     4    {
     5    _page.AddOnPreRenderCompleteAsync(new BeginEventHandler(this.BeginExecuteAsyncTasks), new EndEventHandler(this.EndExecuteAsyncTasks));
     6    }

     7    
     8    private IAsyncResult BeginExecuteAsyncTasks(object sender, EventArgs e, AsyncCallback cb, object extraData)
     9    {
    10    return ExecuteTasks(cb, extraData);
    11    }

    12    
    13    private void EndExecuteAsyncTasks(IAsyncResult ar)
    14    {
    15    _asyncResult.End();
    16    }

    17}



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

    to be continue...
  • 相关阅读:
    leetcode108 Convert Sorted Array to Binary Search Tree
    leetcode98 Validate Binary Search Tree
    leetcode103 Binary Tree Zigzag Level Order Traversal
    leetcode116 Populating Next Right Pointers in Each Node
    Python全栈之路Day15
    Python全栈之路Day11
    集群监控
    Python全栈之路Day10
    自动部署反向代理、web、nfs
    5.Scss的插值
  • 原文地址:https://www.cnblogs.com/qixuejia/p/4189340.html
Copyright © 2011-2022 走看看