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 <GetData1>b__6()
{
    Console.WriteLine("1");
}

public void GetData1()
{
    Action action = (CS$<>9__CachedAnonymousMethodDelegate7 != null) ? CS$<>9__CachedAnonymousMethodDelegate7 : (CS$<>9__CachedAnonymousMethodDelegate7 = new Action(Test.<GetData1>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 <GetData2>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.<GetData2>b__4);
}

      可以看見,當引用了局部變量后,Action委托里面的匿名方法就不再編譯為所屬類的一個靜態方法,而是編譯為一個內部類了,然后為這個內部類定義了兩個公共字段i和j,分別對應引用的i與j,而Action真正包裝的,是這個內部類的這個方法<GetData2>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 <GetData3>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.<GetData3>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之異步頁面


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM