C#執行異步操作的幾種方式比較和總結


C#執行異步操作的幾種方式比較和總結

0x00 引言

之前寫程序的時候在遇到一些比較花時間的操作例如HTTP請求時,總是會new一個Thread處理。對XxxxxAsync()之類的方法也沒去了解過,倒也沒遇到什么大問題。最近因為需求要求用DevExpress寫界面,跑起來后發現比Native控件效率差好多。這才想到之前看到的“金科玉律”:不要在UI線程上執行界面無關的操作,因此集中看了下C#的異步操作,分享一下自己的比較和總結。

0x01 測試方法

IDE:VS2015 Community

.NET版本:4.5

使用函數隨機休眠100到500毫秒來模擬耗時任務,並返回任務花費的時間,在UI線程上調用這個方法會造成阻塞,導致UI假死,因此需要通過異步方式執行這個任務,並在信息輸出區域顯示花費的時間。

 

主界面中通過各種不同按鈕測試不同類型的異步操作

 

0x02 使用Thread進行異步操作

使用ThreadPool進行異步操作的方法如下所示,需要注意的就是IsBackground默認為false,也就是該線程對調用它的線程不產生依賴,當調用線程退出時該線程也不會結束。因此需要將IsBackground設置為true以指明該線程是后台線程,這樣當主線程退出時該線程也會結束。另外跨線程操作UI還是要借助Dispatcher.BeginInvoke(),如果需要阻塞UI線程可以使用Dispatcher.Invoke()。

 

0x03 使用ThreadPool進行異步操作

ThreadPool(線程池)的出現主要就是為了提高線程的復用(類似的還有訪問數據庫的連接池)。線程的創建是開銷比較大的行為,為了達到較好的交互體驗,開發中可能會大量使用異步操作,特別是需要頻繁進行大量的短時間的異步操作時,頻繁創建和銷毀線程會在造成很多資源的浪費。而通過在線程池中存放一些線程,當需要新建線程執行操作時就從線程池中取出一個已經存在的空閑線程使用,如果此時沒有空閑線程,且線程池中的線程數未達到線程池上限,則新建一個線程,使用完成后再放回到線程池中。這樣可以極大程度上省去線程創建的開銷。線程池中線程的最小和最大數都可以指定,不過多數情況下無需指定,CLR有一套管理線程池的策略。ThreadPool的使用非常簡單,代碼如下所示。跨線程操作UI仍需借助Dispatcher。

 

0x04 使用Task進行異步操作

Task進行異步操作時也是從線程池中獲取線程進行操作,不過支持的操作更加豐富一些。而且Task<T>可以支持返回值,通過Task的ContinueWith()可以在Task執行結束后將返回值傳入以進行操作,但在ContinueWith中跨線程操作UI仍需借助Dispatcher。另外Task也可以直接使用靜態方法Task.Run<T>()執行異步操作。

 

0x05 使用async/await進行異步操作

這個是C#5中的新特性,當遇到await時,會從線程池中取出一個線程異步執行await等待的操作,然后方法立即返回。等異步操作結束后回到await所在的地方接着往后執行。await需要等待async Task<T>類型的函數。詳細的使用方法可參考相關資料,測試代碼如下所示。異步結束后的會返回到調用線程,所以修改UI不需要Dispatcher。

 

也可以把TestTask包裝成async方法,這樣就可以使用上圖中注釋掉的兩行代碼進行處理。包裝后的異步方法如下所示:

 

async/await也是從線程池中取線程,可實現線程復用,而且代碼簡潔容易閱讀,異步操作返回后會自動返回調用線程,是執行異步操作的首選方式。而且雖然是C#5的新特性,但C#4可以通過下載升級包來支持async/await。

0x06 關於效率

以上嘗試的方法除了直接使用Thread之外,其他幾種都是直接或間接使用線程池來獲取線程的。從理論上來分析,創建線程時要給線程分配棧空間,線程銷毀時需要回收內存,創建線程也會增加CPU的工作。因此可以連續創建線程並記錄消耗的時間來測試性能。測試代碼如下所示:

 

當測試Thread時每次測試在連續創建線程時內存和CPU都會有個小突起,不過在線程結束后很快就會降下去,在我的電腦上連續創建100個線程大概花費120-130毫秒。如圖所示:

 

測試結果:

 

使用基於線程池的方法創建線程時,有時第一次會稍慢一些,應該是線程池內線程不足,時間開銷在0-15毫秒,第一次創建內存也會上升。后面再測試時時間開銷為0毫秒,內存表現也很平穩,CPU開銷分布比較平均。測試結果如圖所示:

 

0x07 結論

在執行異步操作時應使用基於線程池的操作,從代碼的簡潔程度和可讀性上優先使用async/await方式。對於較老的.NET版本可以使用Task或ThreadPool。符合以下情況的可以使用Thread:

1、線程創建后需要持續工作到主線程退出的。這種情況下就算使用線程池線程也不會歸還,實現不了復用,可以使用Thread。

2、線程在主線程退出后仍需要執行的,這種情況使用線程池線程無法滿足需求,需要使用Thread並制定IsBackground為false(默認)。

0x08 相關下載

測試程序代碼在:https://github.com/durow/TestArea/tree/master/AsyncTest/AsyncTest


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM