概述 在之前寫的一篇關於async和await的前世今生的文章之後,大家似乎在async和await提高網站處理能力方面還有一些疑問,博客園本身也做了不少的嘗試。今天我們再來回答一下這個問題,同時我們會做一個async和await在WinForm中的嘗試,並且對比在4.5之前的非同步編程模式APM/E ...
概述
在之前寫的一篇關於async和await的前世今生的文章之後,大家似乎在async和await提高網站處理能力方面還有一些疑問,博客園本身也做了不少的嘗試。今天我們再來回答一下這個問題,同時我們會做一個async和await在WinForm中的嘗試,並且對比在4.5之前的非同步編程模式APM/EAP和async/await的區別,最後我們還會探討在不同線程之間交互的問題。
IIS存在著處理能力的問題,但是WinForm卻是UI響應的問題,並且WinForm的UI線程至始至終都是同一個,所以兩者之間有一定的區別。有人會問,現在還有人寫WinForm嗎?好吧,它確是一個比較老的東西呢,不如WPF炫,技術也不如WPF先進,但是從架構層面來講,不管是Web,還是WinForm,又或是WPF,Mobile,這些都只是表現層,不是麽?現在的大型系統一般桌面客戶端,Web端,手機,平板端都會涉及,這也是為什麼會有應用層,服務層的存在。我們在這談論的ASP.NET MVC,WinForm,WFP,Android/IOS/WP 都是表現層,在表現層我們應該只處理與“表現”相關的邏輯,任何與業務相關的邏輯應該都是放在下層處理的。關於架構的問題,我們後面再慢慢深入,另外別說我沒有提示您,我們今天還會看到.NET中另一個已經老去的技術Web Service。
還得提示您,文章內容有點長,涉及的知識點比較多,所以,我推薦:”先頂後看“ ,先頂後看是21世紀看長篇的首選之道,是良好溝通的開端,想知道是什麼會讓你與眾不同嗎?想知道為什麼上海今天會下這麼大的雨麽?請記住先頂後看,你頂的不是我的文章,而是我們冒著大雨還要去上班的可貴精神!先頂後看,你值得擁有!
目錄
- async/await如何提升IIS響應能力
- 早期Web Service的非同步模式APM
- APM非同步編程詳解
- EAP(Event-Based Asynchroronous Pattern)
- async/await 給WinForm編程帶來了什麼?
- 不同線程之間通訊的問題
- 小結
- 引用 & 擴展閱讀
async/await如何提升IIS處理能力
首先響應能力並不完全是說我們程式性能的問題,有時候可能你的程式沒有任何問題,而且精心經過優化,可是響應能力還是沒有上去,網站性能分析是一個複雜的活,有時候只能靠經驗和不斷的嘗試才能達到比較好的效果。當然我們今天討論的主要是IIS的處理能力,或者也可能說是IIS的性能,但絕非代碼本身的性能。即使async/await能夠提高IIS的處理能力,但是對於用戶來說整個頁面從發起請求到頁面渲染完成的這些時間,是不會因為我們加了async/await之後產生多大變化的。
另外非同步的ASP.NET並非只有async/await才可以做的,ASP.NET在Web Form時代就已經有非同步Page了,包括ASP.NET MVC不是也有非同步的Controller麽?async/await 很新,很酷,但是它也只是在原有一技術基礎上做了一些改進,讓程式員們寫起非同步代碼來更容易了。大家常說微軟喜歡新瓶裝舊酒,至少我們要看到這個新瓶給我們帶來了什麼,不管是任何產品,都不可能一開始就很完美,所以不斷的迭代更新,也可以說是一種正確做事的方式。
ASP.NET並行處理的步驟
ASP.NET是如何在IIS中工作的一文已經很詳細的介紹了一個請求是如何從客戶端到伺服器的HTTP.SYS最後進入CLR進行處理的(強烈建議不瞭解這一塊的同學先看這篇文章,有助於你理解本小節),但是所有的步驟都是基於一個線程的假設下進行的。IIS本身就是一個多線程的工作環境,如果我們從多線程的視角來看會發生什麼變化呢?我們首先來看一下下麵這張圖。註意:我們下麵的步驟是建立在IIS7.0以後的集成模式基礎之上的。(註:下麵這張圖在dudu的提醒之後,重新做了一些搜索工作,做了一些改動,w3dt這一步來自於博客園團隊對問題的不斷探索,詳情可以點這裡)
我們再來梳理一下上面的步驟:
- 所有的請求最開始是由HTTP.SYS接收的,HTTP.SYS內部有一個隊列維護著這些請求,這個隊列的request的數量大於一定數量(預設是1000)的時候,HTTP.SYS就會直接返回503狀態(伺服器忙),這是我們的第一個閥門。性能計數指標:“Http Service Request Queues\CurrentQueueSize”
- 由w3dt負責把請求從HTTP.SYS 的隊列中放到一個對應埠的隊列中,據非官方資料顯示該隊列長度為能為20(該隊列是非公開的,沒有文檔,所以也沒有性能計數器)。
- IIS 的IO線程從上一步的隊列中獲取請求,如果是需要ASP.NET處理的,就會轉交給CLR 線程池的Worker 線程,IIS的IO線程繼續返回重覆做該步驟。CLR 線程池的Worker線程數量是第二個閥門。
- 當CLR中正在被處理的請求數據大於一定值(最大並行處理請求數量,.NET4以後預設是5000)的時候,從IO線程過來的請求就不會直接交給Worker線程,而是放到一個進程池級別的一個隊列了,等到這個數量小於臨界值的時候,才會把它再次交給Worker線程去處理。這是我們的第三個閥門。
- 上一步中說到的那個進程池級別的隊列有一個長度的限制,可以通過web.config裡面的processModel/requestQueueLimit來設置。這可以說也是一個閥門。當正在處理的數量大於所允許的最大並行處理請求數量的時候,我們就會得到503了。可以通過性能計數指標:“ASP.NET v4.0.30319\Requests Queued” 來查看該隊列的長度。
哪些因素會控制我們的響應能力
從上面我們提到了幾大閥門中,我們可以得出下麵的幾個數字控制或者說影響著我們的響應能力。
- HTTP.SYS隊列的長度
- CLR線程池最大Worker線程數量
- 最大並行處理請求數量
- 進程池級別隊列所允許的長度
HTTP.SYS隊列的長度
這個我覺得不需要額外解釋,預設值是1000。這個值取決於我們我們後面IIS IO線程和Worker線程的處理速度,如果它們兩個都處理不了,這個數字再大也沒有用。因為最後他們會被存儲到進程池級別的隊列中,所以只會造成記憶體的浪費。
最大Worker線程數量
這個值是可以在web.config中進行配置的。
maxWorkerThreads: CLR中真實處理請求的最大Worker線程數量
minWorkerThreads:CLR中真實處理請求的最小Worker線程數量
minWorkerThreads的預設值是1,合理的加大他們可以避免不必要的線程創建和銷毀工作。
最大並行處理請求數量
進程池級別的隊列給我們的CLR一定的緩衝,這裡面要註意的是,這個隊列還沒有進入到CLR,所以它不會占用我們托管環境的任何資源,也就是把請求卡在了CLR的外面。我們需要在aspnet.config級別進行配置,我們可以在.net fraemwork的安裝目錄下找到它。一般是 C:\Windows\Microsoft.NET\Framework\v4.0.30319 如果你安裝的是4.0的話。
maxConcurrentRequestPerCPU: 每個CPU所允許的最大並行處理請求數量,當CLR中worker線程正在處理的請求之和大於這個數時,從IO線程過來的請求就會被放到我們進程池級別的隊列中。
maxConcurrentThreadsPerCPU: 設置為0即禁用。
requestQueue: 進程池級別隊列所允許的長度。
async和await 做了什麼?
我們終於要切入正題了,拿ASP.NET MVC舉例,如果不採用async的Action,那麼毫無疑問,它是在一個Woker線程中執行的。當我們訪問一些web service,或者讀文件的時候,這個Worker線程就會被阻塞。假設我們這個Action執行時間一共是100ms,其它訪問web service花了80ms,理想情況下一個Worker線程一秒可以響應10個請求,假設我們的maxWorkerThreads是10,那我們一秒內總是可響應請求就是100。如果說我們想把這個可響應請求數升到200怎麼做呢?
有人會說,這還不簡單,把maxWorkerThreads調20不就行了麽? 其實我們做也沒有什麼 問題,確實是可以的,而且也確實能起到作用。那我們為什麼還要大費周章的搞什麼 async/await呢?搞得腦子都暈了?async/await給我們解決了什麼問題?它可以在我們訪問web service的時候把當前的worker線程放走,將它放回線程池,這樣它就可以去處理其它的請求了。等到web service給我們返回結果了,會再到線程池中隨機拿一個新的woker線程繼續往下執行。也就是說我們減少了那一部分等待的時間,充份利用了線程。
我們來對比一下使用async/awit和不使用的情況,
不使用async/await: 20個woker線程1s可以處理200個請求。
那轉換成總的時間的就是 20 * 1000ms = 20000ms,
其中等待的時間為 200 * 80ms = 16000ms。
也就是說使用async/await我們至少節約了16000ms的時間,這20個worker線程又會再去處理請求,即使按照每個請求100ms的處理時間我們還可以再增加160個請求。而且別忘了100ms是基於同步情況下,包括等待時間在內的基礎上得到的,所以實際情況可能還要多,當然我們這裡沒有算上線程切換的時間,所以實際情況中是有一點差異的,但是應該不會很大,因為我們的線程都是基於線程池的操作。
所有結果是20個Worker線程不使用非同步的情況下,1s能自理200個請求,而使用非同步的情況下可以處理360個請求,立馬提升80%呀!採用非同步之後,對於同樣的請求數量,需要的Worker線程數據會大大減少50%左右,一個線程至少會在堆上分配1M的記憶體,如果是1000個線程那就是1G的容量,雖然記憶體現在便宜,但是省著總歸是好的嘛,而且更少的線程是可以減少線程池在維護線程時產生的CPU消耗的。另:dudu分享 CLR1秒之內只能創建2個線程。
註意:以上數據並非真實測試數據,真實情況一個request的時間也並非100ms,花費在web service上的時間也並非80ms,僅僅是給大家一個思路:),所以這裡面用了async和await之後對響應能力有多大的提升和我們原來堵塞在這些IO和網路上的時間是有很大的關係的。
幾點建議
看到這裡,不知道大家有沒有得到點什麼。首先第一點我們要知道的是async/await不是萬能藥,不們不能指望光寫兩個光鍵字就希望性能的提升。要記住,一個CPU在同一時間段內是只能執行一個線程的。所以這也是為什麼async和await建議在IO或者是網路操作的時候使用。我們的MVC站點訪問WCF或者Web Service這種場景就非常的適合使用非同步來操作。在上面的例子中80ms讀取web service的時間,大部份時間都是不需要cpu操作的,這樣cpu才可以被其它的線程利用,如果不是一個讀取web service的操作,而是一個複雜計算的操作,那你就等著cpu爆表吧。
第二點是,除了程式中利用非同步,我們上面講到的關於IIS的配置是很重要的,如果使用了非同步,請記得把maxWorkerThreads和maxConcurrentRequestPerCPU的值調高試試。
早期對Web service的非同步編程模式APM
講完我們高大上的async/await之後,我們來看看這個技術很老,但是概念確依舊延續至今的Web Service。 我們這裡所說的針對web service的非同步編程模式不是指在伺服器端的web service本身,而是指調用web service的客戶端。大家知道對於web service,我們通過添加web service引用或者.net提供的生成工具就可以生成相應的代理類,可以讓我們像調用本地代碼一樣訪問web service,而所生成的代碼類中對針對每一個web service方法生成3個對應的方法,比如說我們的方法名叫DownloadContent,除了這個方法之外還有BeginDownloadContent和EndDownloadContent方法,而這兩個就是我們今天要說的早期的非同步編程模式APM(Asynchronous Programming Model)。下麵就來看看我們web service中的代碼,註意我們現在的項目都是在.NET Framework3.5下實現的。
PageContent.asmx的代碼
public class PageContent : System.Web.Services.WebService { [WebMethod] public string DownloadContent(string url) { var client = new System.Net.WebClient(); return client.DownloadString(url); } }
註意我們web service中的DownloadContent方法調用的是WebClient的同步方法,WebClient也有非同步方法即:DownloadStringAsync。但是大家要明白,不管伺服器是同步還是非同步,對於客戶端來說調用了你這個web service都是一樣的,就是得等你返回結果。
當然,我們也可以像MVC裡面的代碼一樣,把我們的伺服器端也寫成非同步的。那得到好處的是那個托管web service的伺服器,它的處理能力得到提高,就像ASP.NET一樣。如果我們用JavaScript去調用這個Web Service,那麼Ajax(Asynchronous Javascript + XML)就是我們客戶端用到的非同步編程技術。如果是其它的客戶端呢?比如說一個CS的桌面程式?我們需要非同步編程麽?
當WinForm遇上Web Service
WinForm不像托管在IIS的ASP.NET網站,會有一個線程池管理著多個線程來處理用戶的請求,換個說法ASP.NET網站生來就是基於多線程的。但是,在WinForm中,如果我們不刻意使用多線程,那至始至終,都只有一個線程,稱之為UI線程。也許在一些小型的系統中WinForm很少涉及到多線程,因為WinForm本身的優勢就在它是獨立運行在客戶端的,在性能上和可操作性上都會有很大的優勢。所以很多中小型的WinForm系統都是直接就訪問資料庫了,並且基本上也只有數據的傳輸,什麼圖片資源那是很少的,所以等待的時間是很短的,基本不用費什麼腦力去考慮什麼3秒之內必須將頁面顯示到用戶面前這種問題。
既然WinForm在性能上有這麼大的優勢,那它還需要非同步嗎?
我們上面說的是中小型的WinForm,如果是大型的系統呢?如果WinForm只是其它的很小一部分,就像我們文章開始說的還有很多其它成千上萬個手機客戶端,Web客戶端,平板客戶端呢?如果客戶端很多導致資料庫撐不住怎麼辦? 想在中間加一層緩存怎麼辦?
拿一個b2b的網站功能舉例,用戶可以通過網站下單,手機也可以下單,還可以通過電腦的桌面客戶端下單。在下完單之後要完成交易,庫存扣減,發送訂單確認通知等等功能,而不管你的訂單是通過哪個端完成的,這些功能我們都要去做,對嗎?那我們就不能單獨放在WinForm裡面了,不然這些代碼在其它的端裡面又得全部全新再一一實現,同樣的代碼放在不同的地方那可是相當危險的,所以就有了我們後來的SOA架構,把這些功能都抽成服務,每種類型的端都是調用服務就可以了。一是可以統一維護這些功能,二是可以很方便的做擴展,去更好的適應功能和架構上的擴展。比如說像下麵這樣的一個系統。
在上圖中,Web端雖然也是屬於我們平常說的服務端(甚至是由多台伺服器組成的web群集),但是對我們整個系統來說,它也只是一個端而已。對於一個端來說,它本身只處理和用戶交互的問題,其餘所有的功能,業務都會交給後來台處理。在我們上面的架構中,應用層都不會直接參加真正業務邏輯相關的處理,而是放到我們更下層數據層去做處理。那麼應用層主要協助做一些與用戶交互的一些功能,如果手機簡訊發送,郵件發送等等,並且可以根據優先順序選擇是放入隊列中稍候處理還是直接調用功能服務立即處理。
在這樣的一個系統中,我們的Web伺服器也好,Winform端也好都將只是整個系統中的一個終端,它們主要的任何是用戶和後面服務之間的一個橋梁。涉及到Service的調用之後,為了給用戶良好的用戶體驗,在WinForm端,我們自然就要考慮非同步的問題。
WinForm非同步調用Web Service
有了像VS這樣強大的工具為我們生成代理類,我們在寫調用Web service的代碼時就可以像調用本地類庫一樣調用Web Service了,我們只需要添加一個Web Reference就可以了。
// Form1.cs的代碼
private void button1_Click(object sender, EventArgs e) { var pageContentService = new localhost.PageContent(); pageContentService.BeginDownloadContent( "http://jesse2013.cnblogs.com", new AsyncCallback(DownloadContentCallback), pageContentService); } private void DownloadContentCallback(IAsyncResult result) { var pageContentService = (localhost.PageContent)result.AsyncState; var msg = pageContentService.EndDownloadContent(result); MessageBox.Show(msg); }
代碼非常的簡單,在執行完pageContentService.BeginDownloadContent之後,我們的主線程就返回了。在調用Web service這段時間內我們的UI不會被阻塞,也不會出現“無法響應這種情況”,我們依然可以拖動窗體甚至做其它的事情。這就是APM的魔力,但是我們的callback究竟是在哪個線程中執行的呢?是線程池中的線程麽?咋們接著往下看。
APM非同步編程模式詳解
線程問題
接下來我們就是更進一步的瞭解APM這種模式是如何工作的,但是首先我們要回答上面留下來的問題,這種非同步的編程方式有沒有為我們開啟新的線程?讓代碼說話:
private void button1_Click(object sender, EventArgs e) { Trace.TraceInformation("Is current thread from thread pool? {0}", Thread.CurrentThread.IsThreadPoolThread ? "Yes" : "No"); Trace.TraceInformation("Start calling web service on thread: {0}", Thread.CurrentThread.ManagedThreadId); var pageContentService = new localhost.PageContent(); pageContentService.BeginDownloadContent( "http://jesse2013.cnblogs.com", new AsyncCallback(DownloadContentCallback), pageContentService); } private void DownloadContentCallback(IAsyncResult result) { var pageContentService = (localhost.PageContent)result.AsyncState; var msg = pageContentService.EndDownloadContent(result); Trace.TraceInformation("Is current thread from thread pool? {0}" , Thread.CurrentThread.IsThreadPoolThread ? "Yes" : "No"); Trace.TraceInformation("End calling web service on thread: {0}, the result of the web service is: {1}", Thread.CurrentThread.ManagedThreadId, msg); }
我們在按鈕點擊的方法和callback方法中分別輸出當前線程的ID,以及他們是否屬於線程池的線程,得到的結果如下:
Desktop4.0.vshost.exe Information: 0 : Is current thread a background thread? NO
Desktop4.0.vshost.exe Information: 0 : Is current thread from thread pool? NO
Desktop4.0.vshost.exe Information: 0 : Start calling web service on thread: 9
Desktop4.0.vshost.exe Information: 0 : Is current thread a background thread? YES
Desktop4.0.vshost.exe Information: 0 : Is current thread from thread pool? YES
Desktop4.0.vshost.exe Information: 0 : End calling web service on thread: 14, the result of the web service is: <!DOCTYPE html>...
按鈕點擊的方法是由UI直接控制,很明顯它不是一個線程池線程,也不是後臺線程。而我們的callback卻是在一個來自於線程池的後臺線程執行的,答案揭曉了,可是這會給我們帶來一個問題,我們上面講了只有UI線程也可以去更新我們的UI控制項,也就是說在callback中我們是不能更新UI控制項的,那我們如何讓更新UI讓用戶知道反饋呢?答案在後面接曉 :),讓我們先專註於把APM弄清楚。
從Delegate開始
其實,APM在.NET3.5以前都被廣泛使用,在WinForm窗體控制中,在一個IO操作的類庫中等等!大家可以很容易的找到搭配了Begin和End的方法,更重要的是只要是有代理的地方,我們都可以使用APM這種模式。我們來看一個很簡單的例子:
delegate void EatAsync(string food); private void button2_Click(object sender, EventArgs e) { var myAsync = new EatAsync(eat); Trace.TraceInformation("Activate eating on thread: {0}", Thread.CurrentThread.ManagedThreadId); myAsync.BeginInvoke("icecream", new AsyncCallback(clean), myAsync); } private void eat(string food) { Trace.TraceInformation("I am eating.... on thread: {0}", Thread.CurrentThread.ManagedThreadId); } private void clean(IAsyncResult asyncResult) { Trace.TraceInformation("I am done eating.... on thread: {0}", Thread.CurrentThread.ManagedThreadId); }
上面的代碼中,我們通過把eat封裝成一個委托,然後再調用該委托的BeginInvoke方法實現了非同步的執行。也就是實際的eat方法不是在主線程中執行的,我們可以看輸出的結果:
Desktop4.0.vshost.exe Information: 0 : Activate eating on thread: 10
Desktop4.0.vshost.exe Information: 0 : I am eating.... on thread: 6
Desktop4.0.vshost.exe Information: 0 : I am done eating.... on thread: 6
clean是我們傳進去的callback,該方法會在我們的eat方法執行完之後被調用,所以它會和我們eat方法在同一個線程中被調用。大家如果熟悉代理的話就會知道,代碼實際上會被編譯成一個類,而BeginInvoke和EndInvoke方法正是編譯器為我們自動加進去的方法,我們不用額外做任何事情,這在早期沒有TPL和async/await之前(APM從.NET1.0時代就有了),的確是一個不錯的選擇。
再次認識APM
瞭解了Delegate實現的BeginInvoke和EndInvoke之後,我們再來分析一下APM用到的那些對象。 拿我們Web service的代理類來舉例,它為我們生成了以下3個方法:
- string DownloadContent(string url): 同步方法
- IAsyncResult BeginDownloadContent(string url, AsyncCallback callback, object asyncState): 非同步開始方法
- EndDownloadContent(IAsyncResult asyncResult):非同步結束方法
在我們調用EndDownloadContent方法的時候,如果我們的web service調用還沒有返回,那這個時候就會用阻塞的方式去拿結果。但是在我們傳到BeginDownloadContent中的callback被調用的時候,那操作一定是已經完成了,也就是說IAsyncResult.IsCompleted = true。而在APM非同步編程模式中Begin方法總是返回IAsyncResult這個介面的實現。IAsyncReuslt僅僅包含以下4個屬性:
WaitHanlde通常作為同步對象的基類,並且可以利用它來阻塞線程,更多信息可以參考MSDN 。 藉助於IAsyncResult的幫助,我們就可以通過以下幾種方式去獲取當前所執行操作的結果。
- 輪詢
- 強制等待
- 完成通知
完成通知就是們在"WinForm非同步調用WebService"那結中用到的方法,調完Begin方法之後,主線程就算完成任務了。我們也不用監控該操作的執行情況,當該操作執行完之後,我們在Begin方法中傳進去的callback就會被調用了,我們可以在那個方法中調用End方法去獲取結果。下麵我們再簡單說一下前面兩種方式。
//輪詢獲取結果代碼
var pageContentService = new localhost.PageContent(); IAsyncResult asyncResult = pageContentService.BeginDownloadContent( "http://jesse2013.cnblogs.com", null, pageContentService); while (!asyncResult.IsCompleted) { Thread.Sleep(100); } var content = pageContentService.EndDownloadContent(asyncResult);
// 強制等待結果代碼
var pageContentService = new localhost.PageContent(); IAsyncResult asyncResult = pageContentService.BeginDownloadContent( "http://jesse2013.cnblogs.com", null, pageContentService); // 也可以調用WaitOne()的無參版本,不限制強制等待時間 if (asyncResult.AsyncWaitHandle.WaitOne(2000)) { var content = pageContentService.EndDownloadContent(asyncResult); } else { // 2s時間已經過了,但是還沒有執行完 }
EAP(Event-Based Asynchronous Pattern)
EAP是在.NET2.0推出的另一種過渡的非同步編程模型,也是在.NET3.5以後Microsoft支持的一種做法,為什麼呢? 如果大家建一個.NET4.0或者更高版本的WinForm項目,再去添加Web Reference就會發現生成的代理類中已經沒有Begin和End方法了,記住在3.5的時候是兩者共存的,你可以選擇任意一種來使用。但是到了.NET4.0以後,EAP成為了你唯一的選擇。(我沒有嘗試過手動生成代理類,有興趣的同學可以嘗試一下)讓我們來看一下在.NET4下,我們是如何非同步調用Web Service的。
private void button1_Click(object sender, EventArgs e) { var pageContent = new localhost.PageContent(); pageContent.DownloadContentAsync("http://jesse2013.cnblogs.com"); pageContent.DownloadContentCompleted += pageContent_DownloadContentCompleted; } private void pageContent_DownloadContentCompleted(object sender, localhost.DownloadContentCompletedEventArgs e) { if (e.Error == null) { textBox1.Text = e.Result; } else { // 出錯了 } }
線程問題
不知道大家還是否記得,在APM模式中,callback是執行在另一個線程中,不能隨易的去更新UI。但是如果你仔細看一下上面的代碼,我們的DownloadContentCompleted事件綁定的方法中直接就更新了UI,把返回的內容寫到了一個文本框裡面。通過同樣的方法可以發現,在EAP這種非同步編程模式下,事件綁定的方法也是在調用的那個線程中執行的。也就是說解決了非同步編程的時候UI交互的問題,而且是在同一個線程中執行。 看看下麵的代碼:
private void button1_Click(object sender, EventArgs e) { Trace.TraceInformation("Call DownloadContentAsync on thread: {0}", Thread.CurrentThread.ManagedThreadId); Trace.TraceInformation("Is current from thread pool? : {0}", Thread.CurrentThread.IsThreadPoolThread ? "YES" : "NO"); var pageContent = new localhost.PageContent(); pageContent.DownloadContentAsync("http://jesse2013.cnblogs.com"); pageContent.DownloadContentCompleted += pageContent_DownloadContentCompleted; } private void pageContent_DownloadContentCompleted(object sender, localhost.DownloadContentCompletedEventArgs e) { Trace.TraceInformation("Completed DownloadContentAsync on thread: {0}", Thread.CurrentThread.ManagedThreadId); Trace.TraceInformation("Is current from thread pool? : {0}", Thread.CurrentThread.IsThreadPoolThread ? "YES" : "NO"); }
Desktop4.vshost.exe Information: 0 : Call DownloadContentAsync on thread: 10
Desktop4.vshost.exe Information: 0 : Is current from thread pool? : NO
Desktop4.vshost.exe Information: 0 : Completed DownloadContentAsync on thread: 10
Desktop4.vshost.exe Information: 0 : Is current from thread pool? : NO
async/await 給WinFrom帶來了什麼
如果說async給ASP.NET帶來的是處理能力的提高,那麼在WinForm中給程式員帶來的好處則是最大的。我們再也不用因為要實現非同步寫回調或者綁定事件了,省事了,可讀性也提高了。不信你看下麵我們將調用我們那個web service的代碼在.NET4.5下實現一下:
private async void button2_Click(object sender, EventArgs e) { var pageContent = new localhost.PageContentSoapClient(); var content = await pageContent.DownloadContentAsync("http://jesse2013.cnblogs.com"); textBox1.Text = content.Body.DownloadContentResult; }
簡單的三行代碼,像寫同步代碼一樣寫非同步代碼,我想也許這就是async/await的魔力吧。在await之後,UI線程就可以回去響應UI了,在上面的代碼中我們是沒有新線程產生的,和EAP一樣拿到結果直接就可以對UI操作了。
async/await似乎真的很好,但是如果我們await後面的代碼執行在另外一個線程中會發生什麼事情呢?
private async void button1_Click(object sender, EventArgs e) { label1.Text = "Calculating Sqrt of 5000000"; button1.Enabled = false; progressBar1.Visible = true; double sqrt = await Task<double>.Run(() => { double result = 0; for (int i = 0; i < 50000000; i++) { result += Math.Sqrt(i); progressBar1.Maximum = 50000000; progressBar1.Value = i; } return result; }); progressBar1.Visible = false; button1.Enabled = true; label1.Text = "The sqrt of 50000000 is " + sqrt; }
我們在界面中放了一個ProgressBar,同時開一個線程去把從1到5000000的平方全部加起來,看起來是一個非常耗時的操作,於是我們用Task.Run開了一個新的線程去執行。(註:如果是純運算的操作,多線程操作對性能沒有多大幫助,我們這裡主要是想給UI一個進度顯示當前進行到哪一步了。)看起來沒有什麼問題,我們按F5運行吧!
Bomb~
當執行到這裡的時候,程式就崩潰了,告訴我們”無效操作,只能從創建porgressBar的線程訪問它。“ 這也是我們一開始提到的,在WinForm程式中,只有UI主線程才能對UI進行操作,其它的線程是沒有許可權的。接下來我們就來看看,如果在WinForm中實現非UI線程對UI控制的更新操作。
不同線程之間通訊的問題
萬能的Invoke
WinForm中絕大多數的控制項包括窗體在內都實現了Invoke方法,可以傳入一個Delegate,這個Delegate將會被擁有那個控制的線程所調用,從而避免了跨線程訪問的問題。
Trace.TraceInformation("UI Thread : {0}", Thread.CurrentThread.ManagedThreadId); double sqrt = await Task<double>.Run(() => { Trace.TraceInformation("Run calculation on thread: {0}", Thread.CurrentThread.ManagedThreadId); double result = 0; for (int i = 0; i < 50000000; i++) { result += Math.Sqrt(i); progressBar1.Invoke(new Action(() => { Trace.TraceInformation("Update UI on thread: {0}", Thread.CurrentThread.ManagedThreadId); progressBar1.Maximum = 50000000; progressBar1.Value = i; })); } return result; });
Desktop.vshost.exe Information: 0 : UI Thread : 9
Desktop.vshost.exe Information: 0 : Run calculation on thread: 10
Desktop.vshost.exe Information: 0 : Update UI on thread: 9
Invoke方法比較簡單,我們就不做過多的研究了,但是我們要考慮到一點,Invoke是WinForm實現的UI跨線程溝通方式,WPF用的卻是Dispatcher,如果是在ASP.NET下跨線程之間的同步又怎麼辦呢。為了相容各種技術平臺下,跨線程同步的問題,Microsoft在.NET2.0的時候就引入了我們下麵的這個對象。
SynchronizationContext上下文同步對象
為什麼需要SynchronizationContext
就像我們在WinForm中遇到的問題一樣,有時候我們需要在一個線程中傳遞一些數據或者做一些操作到另一個線程。但是在絕大多數情況下這是不允許的,出於安全因素的考慮,每一個線程都有它獨立的記憶體空間和上下文。因此在.NET2.0,微軟推出了SynchronizationContext。
它主要的功能之一是為我們提供了一種將一些工作任務(Delegate)以隊列的方式存儲在一個上下文對象中,然後把這些上下文對象關聯到具體的線程上,當然有時候多個線程也可以關聯到同一個SynchronizationContext對象。獲取當前線程的同步上下文對象可以使用SynchronizationContext.Current。同時它還為我們提供以下兩個方法Post和Send,分別是以非同步和同步的方法將我們上面說的工作任務放到我們SynchronizationContext的隊列中。
SynchronizationContext示例
還是拿我們上面Invoke中用到的例子舉例,只是這次我們不直接調用控制項的Invoke方法去更新它,而是寫了一個Report的方法專門去更新UI。
double sqrt = await Task<double>.Run(() => { Trace.TraceInformation("Current thread id is:{0}", Thread.CurrentThread.ManagedThreadId); double result = 0; for (int i = 0; i < 50000000; i++) { result += Math.Sqrt(i); Report(new Tuple<int, int>(50000000, i)); } return result; });
每一次操作完之後我們調用一下Report方法,把我們總共要算的數字,以及當前正在計算的數字傳給它就可以了。接下來就看我們的Report方法了。
private SynchronizationContext m_SynchronizationContext; private DateTime m_PreviousTime = DateTime.Now; public Form1() { InitializeComponent(); // 在全局保存當前UI線程的SynchronizationContext對象 m_SynchronizationContext = SynchronizationContext.Current; } public void Report(Tuple<int, int> value) { DateTime now = DateTime.Now; if ((now - m_PreviousTime).Milliseconds > 100) { m_SynchronizationContext.Post((obj) => { Tuple<int, int> minMax = (Tuple<int, int>)obj; progressBar1.Maximum = minMax.Item1; progressBar1.Value = minMax.Item2; }, value); m_PreviousTime = now; } }
整個操作看起來要比Inovke複雜一點,與Invoke不同的是SynchronizationContext不需要對Control的引用,而Invoke必須先得有那個控制項才能調用它的Invoke方法對它進行操作。
小結
這篇博客內容有點長,不知道有多少人可以看到這裡:)。最開始我只是想寫寫WinFrom下非同步調用Web Service的一些東西,在一開始這篇文件的題目是”非同步編程在WinForm下的實踐“,但是寫著寫著發現越來越多的迷團沒有解開,其實都是一些老的技術以前沒有接觸和掌握好,所以所幸就一次性把他們都重新學習了一遍,與大家分享。
我們再來回顧一下文章所涉及到的一些重要的概念:
- async/await 在ASP.NET做的最大貢獻(早期ASP.NET的非同步開發模式同樣也有這樣的貢獻),是在訪問資料庫的時候、訪問遠程IO的時候及時釋放了當前的處理性程,可以讓這些線程回到線程池中,從而實現可以去處理其它請求的功能。
- 非同步的ASP.NET開發能夠在處理能力上帶來多大的提高,取決於我們的程式有多少時間是被阻塞的,也就是那些訪問資料庫和遠程Service的時間。
- 除了將代碼改成非同步,我們還需要在IIS上做一些相對的配置來實現最優化。
- 不管是ASP.NET、WinForm還是Mobile、還是平板,在大型系統中都只是一個與用戶交互的端而已,所以不管你現在是做所謂的前端(JavaScript + CSS等),還是所謂的後端(ASP.NET MVC、WCF、Web API 等 ),又或者是比較時髦的移動端(IOS也好,Andrioid也罷,哪怕是不爭氣的WP),都只是整個大型系統中的零星一角而已。當然我並不是貶低這些端的價值,正是因為我們專註於不同,努力提高每一個端的用戶體驗,才能讓這些大型系統有露臉的機會。我想說的是,在你對現在技術取得一定的成就之後,不要停止學習,因為整個軟體架構體系中還有很多很多美妙的東西值得我們去發現。
- APM和EAP是在async/await之前的兩種不同的非同步編程模式。
- APM如果不阻塞主線程,那麼完成通知(回調)就會執行在另外一個線程中,從而給我們更新UI帶來一定的問題。
- EAP的通知事件是在主線程中執行的,不會存在UI交互的問題。
- 最後,我們還學習了在Winform下不同線程之間交互的問題,以及SynchronizationContext。
- APM是.NET下最早的非同步編程方法,從.NET1.0以來就有了。在.NET2.0的時候,微軟意識到了APM的回調函數中與UI交互的問題,於是帶來了新的EAP。APM與EAP一直共存到.NET3.5,在.NET4.0的時候微軟帶來了TPL,也就是我們所熟知的Task編程,而.NET4.5就是我們大家知道的async/await了,可以看到.NET一直在不停的進步,加上最近不斷的和開源社區的合作,跨平臺等特性的引入,我們有理由相信.NET會越走越好。
最後,這篇文章從找資料學習到寫出來,差不多花了我兩個周未的時間,希望能夠給需要的人或者感興趣想要不斷學習的人一點幫助(不管是往前學習,還是往後學習)最後還要感謝@田園裡面的蟋蟀,在閱讀的時候給我找了一些錯別字!
引用 & 擴展閱讀
http://blogs.msdn.com/b/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net-applications.aspx
http://blog.stevensanderson.com/2008/04/05/improve-scalability-in-aspnet-mvc-using-asynchronous-requests
http://blogs.msdn.com/b/tmarq/archive/2007/07/21/asp-net-thread-usage-on-iis-7-0-and-6-0.aspx
http://blogs.msdn.com/b/tmarq/archive/2010/04/14/performing-asynchronous-work-or-tasks-in-asp-net-applications.aspx
http://mohamadhalabi.com/2014/05/08/thread-throttling-in-iis-hosted-wcf-sync-vs-async/
Pro Asynchronous Programs with .NET by Richard Blewett and Andrew Clymer