asp.net非同步處理機制研究

來源:互聯網
上載者:User

      前幾天看了兩篇寫的非常好的博文:詳解.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之非同步頁面

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.