“發現問題的能力,運用技術解決問題的能力,是一個技術人成長的關鍵”
@圖片故事:洋姜的花,拍攝于2022年7月23日,地點:北京奧林匹克森林公園 ,攝影師:劉先生
概要:使用C#發起多線程任務十分簡單,本文旨在匯總多線程編程的注意事項,重點不在于如何發起多線程,主要內容如下:
控制線程并發數量
界定共享資源
加鎖并控制鎖范圍
子線程異常處理
未完成任務取消
希望對小伙伴兒們有所幫助
01
—
控制線程并發數量
多線程以多任務并行的方式,加快業務處理速度,但如果線程數量超出了系統的承載能力,反倒會造成系統整體性能下降,如何合理地控制線程并發數量,是多線程開發的關鍵。
推薦采用信號量機制,可以在線程總數未知的情況下,有效地控制并發線程數量,并且以瀑布流的形式,連續執行后續線程,邏輯清晰可控,執行性能高效。
基礎代碼邏輯如下:
//semaphoreCount是設定的可并行運行的最大線程數量
//taskCount是需要發起的線程的數量
using (Semaphore semaphore = new Semaphore(semaphoreCount, semaphoreCount))
{var woker = new Worker();Task[] tasks = new Task[taskCount];for (int step = 0; step < taskCount; step++) { //獲取一個信號量,如果所有信號量都已使用,則等待直到一個被釋放 semaphore.WaitOne(); //獲得信號量之后,才能發起子線程 tasks[step] = Task.Factory.StartNew((data) => { woker.Work(data); }, innerData).ContinueWith((task) => { //線程完成,釋放信號量 semaphore.Release(); }); } //...
}
簡單來說,是由于分時操作系統,多任務之間存在線程上下文切換,有興趣的同學可以嘗試一下,一次性啟動2000個以上線程,查看計算機的資源耗用情況,以便有更真切的體會。
02
—
界定共享資源
線程共享資源,一類是業務本身需要多個子線程共同處理的資源,另一類是從性能角度考慮,需要被多個子線程共享的資源。
以數據查詢為例,數據庫連接是一種昂貴的資源,如果每個子線程單獨創建數據庫連接,必然會造成浪費,多個線程共用一個數據庫連接是更合理的選擇,因此,數據庫連接便是共享資源。
有興趣的同學可以測試一下,同時啟動50個以上線程,如果每個線程創建一個數據庫連接,會造成數據庫短時間內無法創建足夠連接而報錯。
03
—
加鎖并控制鎖范圍
對共享資源進行訪問時,需要加鎖保護,防止并發錯誤。
對于業務本身處理的共享資源,加鎖主要是防止數據處理錯誤;對于集合類型的共享資源,建議首選System.Collections.Concurrent?命名空間下的集合類型,以達到線程安全的目的;對于如數據庫連接之類的資源,加鎖是為了防止程序異常,如數據庫連接、HttpClient對象,在一個請求處理完之前,是不能被其他線程訪問的,因此需要加鎖,確保串行訪問是必須的。
對于鎖對象,推薦的寫法如下,至于是不是要加static?,要看具體業務場景,靜態變量的作用域是整個應用程序,如果有兩個以上請求同時到達,那么在訪問到加鎖代碼塊時,請求也是串行執行的,普通變量的作用域是當前對象,鎖范圍也是在當前對象內,請求間相互不影響。
readonly?object?locker?=?new?object();
04
—
子線程異常處理
概括成一句話是:在明確異常處理要做什么的情況下,才進行異常處理,否則,讓異常拋出,交由外層程序處理即可。參考我上一篇文章:異常處理,究竟是處理什么
多線程下異常處理的不同之處在于:子線程內的異常,不會直接拋出到主線程,而是保存在了Task對象的Exception屬性中。因此,需要開發小伙伴判斷線程狀態,進行異常處理。
基礎代碼邏輯如下:
Task.Factory.StartNew((data) => { woker.Work(data); }, innerData) .ContinueWith((task) => {????????????????//判斷線程處理狀態,如果執行失敗,則拋出異常????????????????if (task.Status == TaskStatus.Faulted)????????????????{????????????????????throw?task.Exception; }??????});
05
—
未完成任務取消
當某個子線程發生異常之后,取消后續相關線程的執行,符合絕大多數業務邏輯。
取消線程操作需要用到?CancellationTokenSource?類,線程啟動時,注冊“取消憑證(Token)”,當某個子線程發生異常后,調用CancellationTokenSource的Cancel()方法,通知相關線程取消操作。以后會寫一篇CancellationToken的詳細介紹。
基礎代碼邏輯如下:
//聲明 CancellationTokenSource
using (CancellationTokenSource cancellation = new CancellationTokenSource())
{Task[] tasks = new Task[taskCount];for (int step = 0; step < steps; step++){semaphore.WaitOne();//注冊cancellation.Tokentasks[step] = Task.Factory.StartNew((data) => { woker.Work(data); }, innerData, cancellation.Token).ContinueWith((task) =>{if (task.Status == TaskStatus.Faulted){//通知取消任務cancellation.Cancel(true);throw task.Exception;}semaphore.Release();});}
}
有多線程開發經歷的小伙伴,可以看一下自己的代碼,是否有對以上幾點的處理。以上內容均來自于我個人的經驗總結,如有疏漏,歡迎小伙伴補充指正。
最后,說一下對于多線程的認識,了解二次元的小伙伴應該知道一個詞:“結界”,線程與結界有很多相似之處,一個子線程就相當于一個結界,結界內外雖處于同一空間,但卻屬于不同的世界,結界阻斷了結界內外的聯系,但又可以相互作用,更多相似處,小伙伴們自己體會。
您的反饋是我堅持的動力,歡迎點贊,轉發,關注