1. 程式人生 > >C#中實現並發的幾種方法的性能測試

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

返回 也不會 thead syn image 9.png 結果 次數 存在

原文地址:https://www.cnblogs.com/durow/p/4837746.html

0x00 起因

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

輪詢的方式通過線程池實現了線程的復用,可以肯定的是在資源開銷上肯定是小很多的,但輪詢的方式在單位時間內的處理次數會不會比保持線程的方式少很多呢,本測試將解決這個疑問。

0x01 實驗方法

IDE:VS2015

.Net Framework 4.5

接收數據的對象如下所示

技術分享圖片

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

0x02 保持線程的並發

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

技術分享圖片

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)循環,只要寫循環體即可。

6、補充測試

經測試隨著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

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