標籤:基於 命令 連結 沒有 vs2015 運行 防止 行資料 空間
原文地址: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#中實現並發的幾種方法的效能測試