天天看點

C#中實作并發的幾種方法的性能測試

去年寫的一個程式因為需要在區域網路發送消息支援一些指令和簡單資料的傳輸,是以寫了一個C/S的通信子產品。當時的做法很簡單,服務端等待連結,有使用者接入後開啟一個線程,線上程中運作一個while循環接收資料,接收到資料就處理。使用者退出(收到QUIT指令)後線程結束。程式一直運作正常(當然還要處理“TCP粘包”、消息格式封裝等問題,在此不作讨論),不過随着使用的人越來越多,而且考慮到線程開銷比較大,如果有100個使用者連結那麼服務端就要多建立100個線程,500個使用者就是500個線程,确實太誇張了(當然實際并沒有那麼多使用者)。由于TCP通信并不是每時每刻都在進行着的,是以可以把所有用戶端連接配接存儲到一個清單中,通過輪詢的方式依次開啟一個線程進行資料接收,接收完畢後釋放線程,這樣可以充分利用線程池,避免大量線程消耗記憶體和CPU。

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

IDE:VS2015

.Net Framework 4.5

接收資料的對象如下所示

C#中實作并發的幾種方法的性能測試

通過ReceiveData方法接收資料,每次接收隻有1%的可能性收到資料,通過建立N個對象接收資料來模拟一個TCP服務端處理N個連接配接的情況。畢竟TCP通信不是随時進行的,當然這個百分比可以調整。程式輸出的内容包括每秒執行了多少次接收操作,接收到資料的線程編号和接收到的内容等。

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

C#中實作并發的幾種方法的性能測試

方法是使用一個List(或其他容器)把所有的對象放進去,建立一個線程(為了防止UI假死,由于這個線程建立後會一直執行切運算密集,是以使用TheadPool和Thread差别不大),在這個線程中使用foreach(或for)循環依次對每個對象執行ReceiveData方法,每次執行的時候建立一個線程池線程來執行。代碼如下:

C#中實作并發的幾種方法的性能測試

方法與ThreadPool類似,隻是每次建立線程池線程執行ReceiveData方法時是通過Task建立的線程。代碼如下所示:

C#中實作并發的幾種方法的性能測試

方法與ThreadPool類似,隻是每次建立線程池線程執行ReceiveData方法時是通過await等待操作。代碼如下:

剛開始在foreach中寫了await導緻線程阻塞,但因為ReceiveData()中測試時為了盡量拉開差距沒有讓線程睡眠以模拟線程操作,導緻沒有意識到這個問題,多謝 @逸風之狐 提醒。

修改後代碼如下所示,這樣測試方法就可以立即傳回了。不過async/await确實不是用來幹這個的。

C#中實作并發的幾種方法的性能測試

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

C#中實作并發的幾種方法的性能測試

建立500個對象來模拟500個連接配接的情況。其中測試結果中的每秒接收次數會有個波動範圍,主要參照百位以上。使用線程池線程的幾個方法(ThreadPool、Task、await、Parallel)中程式的線程數略有差别,可能跟執行環境有關,難以表明實質性差異。其中await因為線程切換導緻線程執行時間略長,使得線程池需要多建立一些線程。

C#中實作并發的幾種方法的性能測試

平均每秒接收8654次資料。在任務開始後會建立500個線程,由于每個線程都需要單獨的棧空間來執行,記憶體消耗較大。頻繁切換線程也會加重CPU的負擔。

C#中實作并發的幾種方法的性能測試

平均每秒接受9529次資料。由于實作了線程池線程的複用,無需建立太多線程,記憶體沒有出現波動,CPU消耗也比較均勻。

C#中實作并發的幾種方法的性能測試

平均每秒接收9322次資料,由于Task也是基于線程池的封裝,是以與ThreadPool結果差别不大。

C#中實作并發的幾種方法的性能測試

平均每秒接收4150次。await也是使用線程池線程,是以在記憶體開銷和線程數上與其他使用線程池線程的方法沒有太大差别。但await在等待完畢後會将執行上下文從線程池線程切換回調用線程,是以CPU開銷較大。

5、Parallel并發

C#中實作并發的幾種方法的性能測試

看名字就知道這個設計出來就是應用于這種使用環境的,平均每秒接收9387次資料,也是使用線程池線程,是以記憶體和CPU消耗與ThreadPool和Task差不多。但不需要自己寫foreach(for)循環,隻要寫循環體即可。

經測試随着ReceiveData()耗時不斷增加,輪詢方式的優勢越來越小。表現就是剛開始線程執行效率很低,需要花費時間慢慢趕上去。因為線程池中的初始線程不夠用,需要建立更多的線程池線程,線程池線程建立起來沒有Thread那麼快,不過當線程池中的線程數量逐漸滿足需求之後,輪詢的優勢就又展現出來了。

測試1:測試同樣500個線程,有1%的可能接收到資料,但收到資料時模拟執行操作耗時100毫秒,程式剛開始效率很低,花了大概12秒左右,當線程數增長到54個時基本穩定可以滿足需求,效率也越來越高。

C#中實作并發的幾種方法的性能測試

測試2:測試同樣500個線程,有1%的可能接收到資料,但收到資料時模拟執行操作耗時500毫秒,程式剛開始效率同樣很低,花了大概150秒左右,當線程數增長到97個時基本穩定可以滿足需求,效率也越來越高。

C#中實作并發的幾種方法的性能測試

首先明顯能看出來的是使用輪詢的方式比保持線程能節省很多資源,特别是記憶體。而且在處理效率上輪詢的方式(每秒接收9300-9500次)比保持線程還要高(每秒8600+)。是以在這種并發模型下應該使用輪詢的方式以節省資源并提高并發效率。

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

在補充測試中也能看到,不同的運作環境對運作效率的影響還是很大的,是以還是要針對自己的環境做針對性更強的測試以采用更合适的方法。例如在我的使用環境中,服務端TCP消息的轉發和部分指令的處理耗時都是非常短的。同樣假設最高同時線上500個使用者,這500個使用者也不會是同僚登陸的,是以也不會存線上程池初始線程嚴重不夠用的情況。随着使用者慢慢登陸,線程池線程根據需求慢慢增加,這樣建立線程池線程增加的耗時就不那麼明顯了。是以在我的使用環境下輪詢的方式無疑是合适的。是以剛開始對ReceiveData()隻設定了接受資料的機率,沒有模拟延遲。大家有需求的可以把測試程式下下來根據實際情況調整最大并發數、接收到資料的機率和接收資料的耗時以進行測試。

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

更多内容歡迎通路我的部落格:http://www.durow.vip