在前文中,介紹了.NET下的多種異步的形式,在WEB程序中,天生就是多線程的,所以使用異步應該更爲謹慎。本文將着重展開ASP.NET中的異步。編程
【注意】本文中提到的異步指的是服務器端異步,而非客戶端異步(Ajax)。緩存
對於HTTP的請求響應模型,服務器沒法主動通知或回調客戶端,當客戶端發起一個請求後,必須保持鏈接等待服務器的返回結果,才能繼續處理,所以,對於客戶端來講,請求與響應是沒法異步進行,也就是說不管服務器如何處理請求,對於客戶端來講沒有任何差異。服務器
那麼ASP.NET異步指的又是什麼,解決了什麼問題呢?網絡
在解釋ASP.NET異步前,先來考察下ASP.NET線程模型。session
ASP.NET線程模型數據結構
咱們知道,一個WEB服務能夠同時服務器多個用戶,咱們能夠想象一下,WEB程序應該運行於多線程環境中,對於運行WEB程序的線程,咱們能夠稱之爲WEB線程,那麼,先來看看WEB線程長什麼樣子吧。多線程
咱們能夠用一個HttpHandler輸出一些內容。併發
你能夠看到相似於這樣的結果:異步
Name:async
ManagedThreadId:57
IsBackground:True
IsThreadPoolThread:True
這裏能夠看到,WEB線程是一個沒有名稱的線程池中的線程,若是刷新這個頁面,還有機會看到 ManagedThreadId 在不斷變化,而且可能重複出現。說明WEB程序有機會運行於線程池中的不一樣線程。
爲了模擬多用戶併發訪問的狀況,咱們須要對這個處理程序添加人爲的延時,並輸出線程相關信息與開始結束時間,再經過客戶端程序同時發起多個請求,查看返回的內容,分析請求的處理狀況。
咱們用一個命令行程序來發起請求,並顯示結果。
這裏,咱們同時發起了50個請求,而後觀察響應的狀況。
【注意】後面的結果會由於操做系統、IIS版本、管道模式、.NET版本、配置項 的不一樣而不一樣,如下結果爲在Windows Server 2008 R2 + IIS7.5 + .NET 4.5 beta(.NET 4 runtime) + 默認配置 中測試的結果,在沒有特別說明的狀況下,均爲重啓IIS後第一次運行的狀況。
這個程序在個人電腦運行結果是這樣的:
從這個結果大概能夠看出,開始兩個請求幾乎同時開始處理,由於線程池最小線程數爲2(可配置),緊接着後面的請求會每隔半秒鐘開始一個,由於若是池中的線程都忙,會等待半秒(.NET版本不一樣而不一樣),若是仍是沒有線程釋放則開啓新的線程,直到達到最大線程數(可配置)。未能在線程池中處理的請求將被放入請求隊列,當一個線程釋放後,下一個請求緊接着開始在該線程處理。
最終50個請求共產生24個線程,總用時約35.9秒。
光看數據不夠形象,用簡單的代碼把數據轉換成圖形吧,下面是100個請求的處理過程。
咱們能夠看到,當WEB線程長時間被佔用時,請求會因爲線程池而阻塞,同時產生大量的線程,最終響應時間變長。
做爲對比,咱們列出處理時間10毫秒的數據。
共產生線程3個,總用時0.236秒。
根據以上的數據,咱們能夠得出結論,要提升系統響應時間與併發處理數,應儘量減小WEB線程的等待。
【略】請各位自行查驗當一次併發所有處理完畢後再次測試的處理狀況。
【略】請各位自行查驗當處理程序中使用線程池處理等待任務的處理狀況。
如何減小WEB線程的等待呢,那就應該儘早的結果ProcessRequest方法,前一篇中講到,對於一些須要等待完成的任務,可使用異步方法來作,因而咱們能夠在ProcessRequest中調用異步方法,但問題是當ProcessRequest結束後,請求處理也即將結束,一但請求結束,將沒有辦法在這一次請求中返回結果給客戶端,可是此時,異步任務尚未完成,當異步任務完成時,也許再也沒有辦法將結果傳給客戶端了。(難道用輪詢?囧)
咱們須要的方案是,處理請求時能夠暫停處理(不是暫停線程),並保持客戶端鏈接,在須要時,向客戶端輸出結果,並結束請求。
在這個模型中,能夠看到,對於WebServerRuntime來講,咱們的請求處理程序就是一個異步方法,而對於客戶端來講,卻並不知道後面的處理狀況。不管在WebServerRuntime或是咱們的處理程序,都沒有直接佔用線程,一切由什麼時候SetComplete決定。同時能夠看到,這種模式須要WebServerRuntime的緊密配合,提供調用異步方法的接口。在ASP.NET中,這個接口就是IHttpAsyncHandler。
異步ASP.NET處理程序
首先,咱們來實現第一個異步處理程序,在適當的時候觸發結束,在開始和結束時輸出一些信息。
在這裏,咱們實現了一個簡單的AsyncResult,因爲ASP.NET經過回調方法獲取異步完成,不會等待異步,因此不須要WaitHandle。在開始請求時,創建一個AsyncResult後直接返回,當異步完成時,調用AsyncResult的SetComplete方法,調用回調方法,再由ASP.NET調用異步結束。此時整個請求即完成。
當咱們訪問這個地址,能夠獲得相似於下面的結果:
App:11240144 Begin:37:24,2676 ThreadId:6 End:37:29,2619 ThreadId:6
能夠看到開始和結束在同一個線程中運行。
當有多個併發請求時,線程池將忙碌起來,開始與結束處理也獎有機會運行於不一樣的線程上。50個請求併發時的處理數據:
能夠看到,從始至終只由3個線程處理全部的請求,總共時間約5.12秒。
爲簡化分析,咱們用下面的圖來示意異步處理程序的併發處理過程。
這樣,咱們就能夠經過異步的方式,將WEB線程撤底釋放出來。由WEB線程進行請求的接收與結束處理,耗時的操做與等待都進行異步處理。這樣少許的WEB線程就能夠承受大量的併發請求,WEB線程將再也不成爲系統的瓶頸。
在大併發的異步模式下,和前面的數據相比較,能夠看到HttpApplication的對象數量隨併發處理數提升而提升,隨之帶來的一系列數據結構,如HttpHandler緩存,是須要考慮的內存開銷。同時,在異步模式下,請求的完成須要編程的方式來控制,在觸發完成前,客戶端鏈接、HttpContext對象都保持活動狀態,客戶端也一直保持等待,直到超時。所以,異步模式下須要更細緻的資源操做。
咱們來看ASP.NET異步 的典型應用場景。
場景一:處理過程當中有須要等待的任務,而且可使用異步完成的。
這個處理程序讀取服務器的文件並輸出到客戶端。
這是一個簡單的代理,服務器獲取WEB資源後寫回。
在這類程序中,咱們提供的異步處理程序調用了IOCP異步方法,使得大量節省了WEB線程的佔用,相比同步處理程序來講,併發量會獲得至關大的提高。
【注意】前面提到,因爲WEB線程屬於線程池線程,所以,若是在線程池中加入任務,將一樣會影響併發處理數。而在異步處理程序中,由線程池來完成異步將得不到任何本質上的提高,所以在異步處理程序中禁止操做線程池(ThreadPool.QueueUserWorkItem、delegate.BeginInvoke,Task.Run等)。若是肯定須要使用多線程來處理大量的計算,須要本身開啓線程或實現本身的線程池。
上面的代碼將沒法達到異步的效果。
雖然等待工做交由另外一線程去操做,可是該線程與WEB線程性質相同,一樣會致使其餘請求阻塞。
【思考】若是咱們的程序中的確須要有大量的計算,那麼能夠考慮將這些計算提取到獨立的應用服務器中,而後經過網絡IOCP異步調用,達到WEB服務器的高吞吐量與系統的平行擴展性。
典型應用場景二:長鏈接消息推送。
通常來講,在WEB中獲取服務器消息,採用輪詢的方式,這種方式不可避免會有延時,當咱們須要即時消息的推送時(如WEBIM),須要用到長鏈接。
長鏈接方式,由客戶端發起請求,服務器端接收後暫停處理並保持鏈接,當須要發送消息給客戶端時,輸出內容並結束處理,客戶端獲得消息或者超時後,再次發起鏈接。如此達到在HTTP協議上服務器消息即時推送到客戶端的目的。
在這種狀況下,咱們但願服務器儘量長時間保持鏈接,若是採用同步處理程序,則鏈接數受到服務器線程數的限制,而異步處理程序則能夠很好的解決這個問題。異步處理程序開始時,收集相關信息,並放入集合後返回異步結果。當須要向這個客戶端發送消息時,從客戶端集合中找到須要發送的目標,發送完成便可。
首先,咱們須要對客戶端進行標識,這個標識每每採用sessionid來作,本例中簡單起見,經過客戶端傳遞參數獲取。
咱們須要一個集合來保存鏈接中的客戶端,提供一個向這些客戶端發送消息的方法。
對於異步處理程序的開始方法,咱們收集信息並放入集合。
【不完善】因爲客戶端收到一次消息後結束請求,由客戶端再次發起請求,中間會有部分時間間隙,在這間隙中向該客戶端發送的消息將丟失,解決方案是維護另外一個用戶是否在線的表,若是用戶不在線,則處理離線消息,若是在線,而且正在鏈接中,則按上述處理,若是不在鏈接中,則緩存在服務器,當客戶端再次鏈接時,首先檢查緩存的消息,若是有未接消息,則獲取消息並當即返回。
發送消息的處理程序。
能夠在任何須要的位置向客戶端發送消息。
【不完善】咱們須要定時刷新客戶端集合,對於長時間未處理的客戶端進行超時結束處理。
經過異步處理程序構建的長鏈接消息推送機制,單臺服務器能夠輕鬆支持上萬個併發鏈接。
異步Action
在ASP.NET MVC 4中,添加了對異步Action的支持。
在ASP.NET MVC4中,整個處理過程都是異步的。
在圖中能夠看到,最右邊的ActionDescriptor將決定如何調用咱們的Action方法,而如何調用是由具體的Action方法形式決定,ASP.NET MVC會根據不一樣的方法形式建立不一樣的ActionDescriptor實例,從而調用不一樣的處理過程。對於傳統的方法,則使用ReflectedActionDescriptor,他實現Execute方法,調用咱們的Action,並在AsyncControllerActionInvoker包裝成同步調用。而異步調用在ASP.NET MVC 4 中有兩種模式。
異步Action模式一:AsyncController/XXXAsync/XXXCompleted
咱們可使一個Controller繼承自AsyncController,按照約定同時提供兩個方法,分別命名爲XXXAsync/XXXCompleted,ASP.NET MVC則會將他們包裝成ReflectedAsyncActionDescriptor。
因爲沒有IAsyncResult,咱們須要經過AsyncManager來告訴ASP.NET MVC什麼時候完成異步,咱們能夠在方法內部在啓用異步時調用AsyncManager.OutstandingOperations.Increment()告訴ASP.NET MVC開始了一次異步,完成異步時調用AsyncManager.OutstandingOperations.Decrement()告訴ASP.NET MVC完成了一次異步,當全部異步完成,AsyncManager會自動觸發異步完成事件,調用回調方法,最終調用咱們的XXXComplete方法。咱們也能夠用AsyncManager.Finish()也觸發全部異步完成。當不使用任何AsyncManager時,則不啓用異步。
能夠看到整個異步過程由ASP.NET完成,在適當的時候會調用咱們的方法。異步的開始、結束動做與及如何觸發完成都在咱們的代碼中體現。
異步Action模式二:Task Action
對於Action,若是返回的類型是 Task,ASP.NET MVC則會將他們包裝成TaskAsyncActionDescriptor。
我只須要需提供一個返回類型爲Task的方法便可,我裏咱們採用async/await語法構建一個異步方法,在方法內部調用其餘的異步方法。
相比以前的模式,簡單了一些,特別是咱們的Controller中,只有一個方法,異步的操做都交由Task完成。對於能夠返回Task的方法來講(如經過async/await包裝多個異步方法),就顯得十分方