热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

asp.net异步处理机制研究

前几天看了两篇写的非常好的博文:详解.NET异步,详解ASP.NET异步.在这两篇文章里,作者详细讲解了如何在.net中进行异步编程以及如何在asp.net中对请求进行异步处理.一开始看

      前几天看了两篇写的非常好的博文:详解.NET异步,详解 ASP.NET异步.在这两篇文章里,作者详细讲解了如何在.net中进行异步编程以及如何在asp.net中对请求进行异步处理.一开始看的时候有很多地方本人都看不懂,或者想不通.借着这股东风,我又重新把asp.net webForm模型复习了一遍,然后阅读了clr via c#,对.net异步处理进行了初步的研究.花了好几天功夫,终于大概能明白整个处理机制了.

      一.asp.net webForm 一般处理流程

      当IIS接收到客户端发来的请求后,如果发现这是请求一个asp.net资源,则通过调用HttpRuntime对像交由.net进行处理.HttpRuntime会创建一个HttpContext对象.这个上下文对象会伴随请求的整个生命周期.然后获取一个HttpApplication对象实例.请注意这里HttpContext对象是创建出来的,而HttpApplication是获取出来的.由于http请求是无状态的,所以在IIS看来,即使是相同的客户端,其每一次的请求也仍然是一次全新的请求.所以上下文对象每次是需要重新创建的.而HttpApplication对象则是处理请求的管道模型对象,只要服务器端的配置不发生变动,这个管道模型的各组件是不会发生变化的,所以不需要每一次都重新创建.为了实现高性能的对像复用.这里就有一个HttpApplication对象池.每当处理完请求后,HttpApplication对象就会重新回到池中以等待下一次被调用.

      上面说过,HttpApplication对象是管道模型对象,所以接下来就是各个HttpModule及真正处理请求的Ihttphandler对象,然后再次经过各个HttpModule对象回到HttpApplication对象,最后向客户端发出响应.

      二.闭包

      所谓闭包,就是使用的变量已经脱离其作用域,却由于和作用域存在上下文关系,从而可以在当前环境中继续使用其上文环境中所定义的一种函数对象.这个东东在动态语言里比较常见,比如Javascript,如:

function f1(){
  
var n=999;
  
return function(){
return n;
  }
}
var a =f1();
alert(a());

      这个局部变量n就是闭包.

      在.net中也有类似的东东.比如说匿名委托就是最常见的.那么在.net中是如何实现闭包语法的呢?这里就要用到反编译工具了.我用的是reflector.记得做一些设置:打开"View"菜单-->选择"Options",先去掉Show PDB symbols前的勾,然后把Optimization后的下拉框改为".Net 1.0"

      (一),不引用外部变量

class Test
{
public void GetData1()
{
Action action1
= new Action(() => Console.WriteLine("1"));
}
}

      经过反编译后的代码如下:

[CompilerGenerated]
private static Action CS$<>9__CachedAnonymousMethodDelegate7;

[CompilerGenerated]
private static void b__6()
{
Console.WriteLine(
"1");
}

public void GetData1()
{
Action action
= (CS$<>9__CachedAnonymousMethodDelegate7 != null) ? CS$<>9__CachedAnonymousMethodDelegate7 : (CS$<>9__CachedAnonymousMethodDelegate7 = new Action(Test.b__6));
}

      可以看见这是最正常的处理,定义一个静态Action委托,一个静态方法,然后进行关联.

      (二).引用局部变量

class Test
{
public void GetData2()
{
int i = 10;
int j = 20;
Action action2
= new Action(() => Console.WriteLine(i + j));
}
}

      经过反编译后的代码如下:

[CompilerGenerated]
private sealed class <>c__DisplayClass5
{
// Fields
public int i;
public int j;

// Methods
public void b__4()
{
Console.WriteLine((
int) (this.i + this.j));
}
}

public void GetData2()
{
<>c__DisplayClass5 class2 = new <>c__DisplayClass5();
class2.i
= 10;
class2.j
= 20;
Action action
= new Action(class2.b__4);
}

      可以看见,当引用了局部变量后,Action委托里面的匿名方法就不再编译为所属类的一个静态方法,而是编译为一个内部类了,然后为这个内部类定义了两个公共字段i和j,分别对应引用的i与j,而Action真正包装的,是这个内部类的这个方法b__4.

      (三).引用所属类的属性

class Test
{
public int Number { get; set; }

public void GetData3()
{
int i = 10;
int j = 20;
Data data
= new Data() { Sum = 10 };
Action action3
= new Action(() => Console.WriteLine(i + j + Number + data.Sum));
}
}

class Data
{
public int Sum { get; set; }
}

      经过反编译后的代码如下:

[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
// Fields
public Test <>4__this;
public Data data;
public int i;
public int j;

// Methods
public void b__1()
{
Console.WriteLine((
int) (this.i + this.j + this.<>4__this.Number + this.data.Sum));
}
}

public void GetData3()
{
<>c__DisplayClass2 class2 = new <>c__DisplayClass2();
class2.
<>4__this = this;
class2.i
= 10;
class2.j
= 20;
Data data
= new Data();
data.Sum
= 10;
class2.data
= data;
Action action
= new Action(class2.b__1);
}

      可以看到,其处理方式基本与第二种情况一样,只不过在内部类里再加了一个所属类的公共字段.

      基本上到这里就清楚了,.net对闭包的实现,实际上是通过构造一个匿名类,把所有用到的资源引用到匿名类的同名共公字段上去来完成的.如上面例三,i,j,number,sum貌似是从Test类引用的,实际上是通用自己的匿名类引用的.这些对象生存周期也仍然没有违反.NET对象生命周期的规则.

      三.从同步到异步

      默认情况下,一个web服务器是可以同时对多个客户端请求进行响应的,显然,web服务器运行于多线程环境中.为了提高处理速度节约资源,他使用了.net的线程池.每当asp.net处理一个客户端请求的时候,其就从线程池里取出一个线程.当处理完成,相关信息返回给客户端的时候,此线程就返回池中以准备下一次的调用.

      如果客户端请求的数据非常复杂,需要经过长时候的计算,那么此线程就会被一直占用.这时如果服务器需要处理新的请求,就必需重新创建一个线程.被占用的线程越多,被占用的内存就越多,CPU上下文切换的次数也越多,性能也就越低下.

      另外还需要说明的是,线程是程序处理的基本单位,我们常说的栈,是在线程上的.程序里所有的资源都必需依附于某个线程.如下图所示:

      首先,线程池为处理asp.net请求调度了一个线程.这个线程处理asp.net生命周期里所涉及到的各个对象.当结果返回给客户端后重新回到线程池等待新的被调用.

      为了提高可能会长时间占用线程的请求的性能,.net提出了异步处理的概念.如下图所示:

      IhttpHandler对象是处理请求的核心对象.既然他处理的时间过长,那么就让他由原来的同步处理变成异步处理,同时把宝贵的线程资源归还给线程池.当异步处理完成后,再重新从线程池中获取一个新的线程完成以后的输出工作.

      四.异步的方式

      在.net中,异步的方式主要是两种:多线程与完成端口,或者跟据clr via C#的说法,叫计算限制与I/O限制.多线程,顾名思义就是利用多个线程并行执行任务,一般跟逻辑计算有关,主要消耗的资源是CPU与内存,所以又叫计算限制;而完成端口则是利用操作系统的特性,利用驱动程序来指导硬件来并行完成任务,比网络传输或文件读写,主要消耗硬件资源,所以又叫I/O限制.

      为了实现这两种异步,.net提供了多种异步编程模型:线程(池),基于线程池的Timer,Task,RegisterWaitForSingleObject,IAsyncResult APM, EAP(基于事件),AsyncEnumerator等等.其实主要就是两种:线程(池)与IAsyncResult APM.前者主要提供对多线程的支持,后者主要提供对完成端口的支持.当然,你在线程(池)里使用完成端口,在IAsyncResult APM里使用线程(池)也是可以的.

      五.asp.net异步

      asp.net异步的核心接口就是IHttpAsyncHandler.它使用的异步模型是IAsyncResult APM.这个接口有两个方法:IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)与void EndProcessRequest(IAsyncResult result),如下图所示:

      可以看到,HttpApplication对象调用了IHttpAsyncHandler对象的BeginProcessRequest方法使用的是一个线程.当BeginProcessRequest方法发起了一个异步调用后,这个线程就回归线程池了.异步调用完成后,重新从线程池里获取一个线程调用一个回调函数,接着调用了EndProcessRequest方法.下面有几小点值得注意.

      (一).对象生存周期.

      上面说过,在程序中所有的资源都需要附着在一个线程上.web线程调完IHttpAsyncHandler对象的BeginProcessRequest方法后就回归线程池了,那么HttpApplication对象是否也已回到了对象池,另一个线程调用HttpApplication对象回调方法,此对象与前一个对象是否是同一个对象呢?经过我的研究,结论是:他们是同一个对象.asp.net异步是通过回调方法来告知异步完成,那么必然就需要把HttpApplication对象回调方法的委托传入异步执行中.一方面,这个传入的过程其实也就是个闭包的过程:异步执行拥有HttpApplication对象的一个委托,HttpApplication对象不会随着web线程的回归而回归或消亡;另一方法,即使你不传入委托,不构成闭包,HttpApplication对象也不会随着web线程的回归而回归或消亡,不会消亡是因为还有HttpApplication对象池线程维持着对他的引用,不会回归则是因为你不回调委托,HttpApplication对象自己也不会智能的回到对象池.

      那么这就引出了另外一个问题:通过异步提高Web服务器的吞吐量的代价是什么.我认为的答案之一是内存占用量增大.原来是一个请求一个HttpApplication对象一个线程,请求:对象:线程=1:1:1,当线程足够大时,额外的请求请排队;现在是所有的请求都能进来,结果就是HttpApplication对象变多并等待处理,线程则处理应该处理的事情;是用HttpApplication对象池对象的增大来换取线程池线程的减少.其实我认为这是值得的,因为HttpApplication对象增多,只是占用了更多的内存,而线程池线程增多,则既占用了更多的内存又占用了更多的CPU.

      (二).asp.net的异步模型为什么是IAsyncResult APM.

      在.net中,一个CLR有且只能有一个线程池.那么意味着web线程就是从这唯一的线程池中来的,这也意味着其它线程池操作的线程来源与web线程的来源是一样的.asp.net异步的本意就是尽可能的释放线程,少占用线程,但是如果你的异步是用另一个线程池线程完成的,那么这和使用同一个线程,对线程池线程的占用量,有什么区别呢,仍然是一个请求占用一个线程,只不过是用两个线程组合起来而以.其性能理应比使用一个线程还要低,因为增加了CPU上下文切换.我想这也就是asp.net团队选取IAsyncResult APM异步模型的原因之一吧.

      当然,这不是否认不能用多线程来完成asp.net异步.在详解 ASP.NET异步这篇博文的留言中我看到了一个园友引用了老外的一些数据并自己总结了结论,我认为说的非常好.其实线程除了来自于线程池,也可以自己去构建,也可以把需要处理的逻辑发往其它机器去处理.只要你使用的线程不来自于线程池,就不会占用web线程或与其产生冲突.就能提高web服务器并发量.当然上面也说过,线程并非越多越好.线程的创建,销毁非常占用系统资源,也会增加CPU上下文切换率.web服务器的并发量并不是直线上升的,而是一个弧线,其增长率会越来越慢,到了一定程度甚至开始下降.在单服务器的情况下,并发量的增大是有限度的.真正想做到大并发量,还是像那么园友说的,使用单独的服务器吧.

      (三).两个线程

      当使用线程来实现异步时,最少会涉及两个线程.如上图所示,asp.net管道模型各对象的执行一直到IHttpAsyncHandler对象的BeginProcessRequest方法是一个线程,用蓝色表示;异步执行,HttpApplication对象的回调方法,IHttpAsyncHandler对象的EndProcessRequest方法及之后的asp.net管道模型各对象的执行是另一个线程,用紫色表示.

      六.应用

      我现在看到的最多的应用就是长连接了.这个例子网上很多,详解 ASP.NET异步这篇博文也写了很多,写的也比较好,这里我就不多说了.

 

      以上就是我的研究心得了,我仔细研究了clr via c#这本书,也参考了很多园友的文章.里面的结论有些来自于参考处,有些则是自己YY的.各位看官还需睁大眼睛.如有说的不对的地方,还请留言告知,水平有限,请多指教!

 

      参考的文章:

      1.详解.NET异步

      2.详解 ASP.NET异步

      3.ASP.NET底层与各个组件的初步认识与理解

      4.C#与闭包

      5.利用Reflector把"闭包"看清楚

      6.用IHttpAsyncHandler实现Comet

      7.你应该知道的 asp.net webform之异步页面


推荐阅读
author-avatar
只爱裙装
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有