C#中實現併發的幾種方法的性能測試

C#中實現併發的幾種方法的性能測試

0x00 原由

去年寫的一個程序由於須要在局域網發送消息支持一些命令和簡單數據的傳輸,因此寫了一個C/S的通訊模塊。當時的作法很簡單,服務端等待連接,有用戶接入後開啓一個線程,在線程中運行一個while循環接收數據,接收到數據就處理。用戶退出(收到QUIT命令)後線程結束。程序一直運行正常(固然還要處理「TCP粘包」、消息格式封裝等問題,在此不做討論),不過隨着使用的人愈來愈多,並且考慮到線程開銷比較大,若是有100個用戶連接那麼服務端就要多建立100個線程,500個用戶就是500個線程,確實太誇張了(固然實際並無那麼多用戶)。因爲TCP通訊並非每時每刻都在進行着的,所以能夠把全部客戶端鏈接存儲到一個列表中,經過輪詢的方式依次開啓一個線程進行數據接收,接收完畢後釋放線程,這樣能夠充分利用線程池,避免大量線程消耗內存和CPU。html

輪詢的方式經過線程池實現了線程的複用,能夠確定的是在資源開銷上確定是小不少的,但輪詢的方式在單位時間內的處理次數會不會比保持線程的方式少不少呢,本測試將解決這個疑問。git

0x01 實驗方法

IDE:VS2015github

.Net Framework 4.5多線程

接收數據的對象以下所示併發

 

經過ReceiveData方法接收數據,每次接收只有1%的可能性收到數據,經過建立N個對象接收數據來模擬一個TCP服務端處理N個鏈接的狀況。畢竟TCP通訊不是隨時進行的,固然這個百分比能夠調整。程序輸出的內容包括每秒執行了多少次接收操做,接收到數據的線程編號和接收到的內容等。異步

0x02 保持線程的併發

保持線程的併發很是直觀,就是每創建一個對象就開一個新線程循環進行ReceiveData操做,當接收到數據就把相關信息輸出到主界面上。代碼以下所示:async

 

0x03 使用ThreadPool輪詢併發

方法是使用一個List(或其餘容器)把全部的對象放進去,建立一個線程(爲了防止UI假死,因爲這個線程建立後會一直執行切運算密集,因此使用TheadPool和Thread差異不大),在這個線程中使用foreach(或for)循環依次對每一個對象執行ReceiveData方法,每次執行的時候建立一個線程池線程來執行。代碼以下:高併發

 

0x04使用Task輪詢併發

方法與ThreadPool相似,只是每次建立線程池線程執行ReceiveData方法時是經過Task建立的線程。代碼以下所示:性能

 

0x05 使用await輪詢併發

方法與ThreadPool相似,只是每次建立線程池線程執行ReceiveData方法時是經過await等待操做。代碼以下:測試

剛開始在foreach中寫了await致使線程阻塞,但由於ReceiveData()中測試時爲了儘可能拉開差距沒有讓線程睡眠以模擬線程操做,致使沒有意識到這個問題,多謝 @逸風之狐 提醒。

修改後代碼以下所示,這樣測試方法就能夠當即返回了。不過async/await確實不是用來幹這個的。

 

0x06 使用Parallel併發

這是FCL提供的一種方法,Parallel.ForEach中每次方法都是異步執行,執行採用的是線程池線程。代碼以下所示:

 

0x07 測試結果

建立500個對象來模擬500個鏈接的狀況。其中測試結果中的每秒接收次數會有個波動範圍,主要參照百位以上。使用線程池線程的幾個方法(ThreadPool、Task、await、Parallel)中程序的線程數略有差異,可能跟執行環境有關,難以代表實質性差別。其中await由於線程切換致使線程執行時間略長,使得線程池須要多建立一些線程。

1、保持線程的併發

 

平均每秒接收8654次數據。在任務開始後會建立500個線程,因爲每一個線程都須要單獨的棧空間來執行,內存消耗較大。頻繁切換線程也會加劇CPU的負擔。

2、ThreadPool輪詢併發

 

平均每秒接受9529次數據。因爲實現了線程池線程的複用,無需建立太多線程,內存沒有出現波動,CPU消耗也比較均勻。

3、Task輪詢併發

 

平均每秒接收9322次數據,因爲Task也是基於線程池的封裝,所以與ThreadPool結果差異不大。

4、await輪詢併發

 

平均每秒接收4150次。await也是使用線程池線程,因此在內存開銷和線程數上與其餘使用線程池線程的方法沒有太大差異。但await在等待完畢後會將執行上下文從線程池線程切換回調用線程,所以CPU開銷較大。

5、Parallel併發

 

看名字就知道這個設計出來就是應用於這種使用環境的,平均每秒接收9387次數據,也是使用線程池線程,因此內存和CPU消耗與ThreadPool和Task差很少。但不須要本身寫foreach(for)循環,只要寫循環體便可。

六、補充測試

經測試隨着ReceiveData()耗時不斷增長,輪詢方式的優點愈來愈小。表現就是剛開始線程執行效率很低,須要花費時間慢慢遇上去。由於線程池中的初始線程不夠用,須要建立更多的線程池線程,線程池線程建立起來沒有Thread那麼快,不過當線程池中的線程數量逐漸知足需求以後,輪詢的優點就又體現出來了。

測試1:測試一樣500個線程,有1%的可能接收到數據,但收到數據時模擬執行操做耗時100毫秒,程序剛開始效率很低,花了大概12秒左右,當線程數增加到54個時基本穩定能夠知足需求,效率也愈來愈高。

測試2:測試一樣500個線程,有1%的可能接收到數據,但收到數據時模擬執行操做耗時500毫秒,程序剛開始效率一樣很低,花了大概150秒左右,當線程數增加到97個時基本穩定能夠知足需求,效率也愈來愈高。

0x08 結論

首先明顯能看出來的是使用輪詢的方式比保持線程能節省不少資源,特別是內存。並且在處理效率上輪詢的方式(每秒接收9300-9500次)比保持線程還要高(每秒8600+)。所以在這種併發模型下應該使用輪詢的方式以節省資源並提升併發效率。

實際上硬拿await來比較是不太公平的,await被設計出來就不是應用於這種場景的。不論是以前關於異步的測試仍是併發的測試,基於線程池的方案相差都不大。所以思路對了的狀況下使用ThreadPool老是沒錯的。但有些類型把ThreadPool包裝了以更好適應某些特殊場景,所以有了Task、await、Parallel等。而在此次的測試條件下顯然Parallel是最合適的,與直接使用ThreadPool相比資源開銷和執行效率同樣,但代碼更少。

在補充測試中也能看到,不一樣的運行環境對運行效率的影響仍是很大的,所以仍是要針對本身的環境作針對性更強的測試以採用更合適的方法。例如在個人使用環境中,服務端TCP消息的轉發和部分命令的處理耗時都是很是短的。一樣假設最高同時在線500個用戶,這500個用戶也不會是同事登錄的,因此也不會存在線程池初始線程嚴重不夠用的狀況。隨着用戶慢慢登錄,線程池線程根據需求慢慢增長,這樣建立線程池線程增長的耗時就不那麼明顯了。因此在個人使用環境下輪詢的方式無疑是合適的。所以剛開始對ReceiveData()只設置了接受數據的機率,沒有模擬延遲。你們有需求的能夠把測試程序下下來根據實際狀況調整最大併發數、接收到數據的機率和接收數據的耗時以進行測試。

0x09 相關下載

測試代碼下載連接:https://github.com/durow/TestArea/tree/master/AsyncTest/ConcurrenceTest

 


更多內容歡迎訪問個人博客:http://www.durow.vip

相關文章
相關標籤/搜索