## 引言 眾所周知,使用線程可以極大的提高應用程式的效率和響應性,提高用戶體驗,但是不可以無節制的使用線程,為什麼呢? ## 線程的開銷 線程的開銷實際上是非常大的,我們從空間開銷和時間開銷上分別討論。 ### 線程的空間開銷 線程的空間開銷來自這四個部分: 1. 線程內核對象(Thread Ke ...
引言
眾所周知,使用線程可以極大的提高應用程式的效率和響應性,提高用戶體驗,但是不可以無節制的使用線程,為什麼呢?
線程的開銷
線程的開銷實際上是非常大的,我們從空間開銷和時間開銷上分別討論。
線程的空間開銷
線程的空間開銷來自這四個部分:
- 線程內核對象(Thread Kernel Object)。每個線程都會創建一個這樣的對象,它主要包含線程上下文信息,在32位系統中,它所占用的記憶體在700位元組左右。
- 線程環境塊(Thread Environment Block)。TEB包括線程的異常處理鏈,32位系統中占用4KB記憶體。
- 用戶模式棧(User Mode Stack),即線程棧。線程棧用於保存方法的參數、局部變數和返回值。每個線程棧占用1024KB的記憶體。要用完這些記憶體很簡單,寫一個不能結束的遞歸方法,讓方法參數和返回值不停地消耗記憶體,很快就會發生
OutOfMemoryException
。 - 內核模式棧(Kernel Mode Stack)。當調用操作系統的內核模式函數時,系統會將函數參數從用戶模式棧複製到內核模式棧。在32位系統中,內核模式棧會占用12KB記憶體。
線程的時間開銷
線程的時間開銷來自這三個過程:
-
線程創建的時候,系統相繼初始化以上這些記憶體空間。
-
接著CLR會調用所有載入DLL的DLLMain方法,並傳遞連接標誌(線程終止的時候,也會調用DLL的DLLMain方法,並傳遞分離標誌)。
-
線程上下文切換。一個系統中會載入很多的進程,而一個進程又包含若幹個線程。但是一個CPU內核在任何時候都只能有一個線程在執行。為了讓每個線程看上去都在運行,系統會不斷地切換“線程上下文”:每個線程及其短暫的執行時間片,然後就會切換到下一個線程了。
這個線程上下文切換過程大概又分為以下5個步驟:
- 步驟1進入內核模式。
- 步驟2將上下文信息(主要是一些CPU寄存器信息)保存到正在執行的線程內核對象上。
- 步驟3系統獲取一個
Spinlock
,並確定下一個要執行的線程,然後釋放Spinlock
。如果下一個線程不在同一個進程內,則需要進行虛擬地址交換。 - 步驟4從將被執行的線程內核對象上傳入上下文信息。
- 步驟5離開內核模式。
所以,由於要進行如此多的工作,所以創建和銷毀一個線程就意味著代價“昂貴”,即使現在的CPU多核多線程,如無節制的使用線程,依舊會嚴重影響性能。
引入線程池
為了免程式員無節制地使用線程,微軟開發了“線程池”技術。簡單來說,線程池就是替開發人員管理工作線程。當一項工作完畢時,CLR不會銷毀這個線程,而是會保留這個線程一段時間,看是否有別的工作需要這個線程。至於何時銷毀或新起線程,由CLR根據自身的演算法來做這個決定。
線程池技術能讓我們重點關註業務的實現,而不是線程的性能測試。
微軟除實現了線程池外,還需要關註一個類型:BackgroundWorker
。 BackgroundWorker
是在內部使用了線程池的技術:同時,在WinForm或WPF編碼中,它還給工作線程和UI線程提供了交互的能力。
實際上, Thread
和 ThreadPool
預設都沒有提供這種交互能力,而 BackgroundWorker
卻通過事件提供了這種能力。這種能力包括:報告進度、支持完成回調、取消任務、暫停任務等。
BackgroundWorker
的簡單示例如下:
private BackgroundWorker backgroundWorker = new BackgroundWorker();
private void AsyncButton_Click(object sender, RoutedEventArgs e)
{
//註冊要執行的任務
backgroundWorker.DoWork += BackgroundWorker_DoWork;
//註冊報告進度
backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged;
//註冊完成時的回調
backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
//設置允許任務取消
backgroundWorker.WorkerSupportsCancellation = true;
//設置允許報告進度
backgroundWorker.WorkerReportsProgress = true;
backgroundWorker.RunWorkerAsync();
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
//取消任務
if (backgroundWorker.IsBusy)
backgroundWorker.CancelAsync();
}
private void BackgroundWorker_RunWorkerCompleted(object? sender, RunWorkerCompletedEventArgs e)
{
//完成時回調
MessageBox.Show("BackgroundWorker RunWorkerCompleted");
}
private void BackgroundWorker_ProgressChanged(object? sender, ProgressChangedEventArgs e)
{
//報告進度
this.textbox.Text = e.ProgressPercentage.ToString();
}
private void BackgroundWorker_DoWork(object? sender, DoWorkEventArgs e)
{
BackgroundWorker? worker = sender as BackgroundWorker;
if (worker != null)
{
for (int i = 0; i < 20; i++)
{
if (worker.CancellationPending)
{
e.Cancel = true;
break;
}
worker.ReportProgress(i);
Thread.Sleep(100);
}
}
}
建議使用WinForm和WPF的開發人員使用 BackgroundWorker
。
Task替代ThreadPool
ThreadPool
相對於 Thread
來說具有很多優勢,但是 ThreadPool
在使用上卻存在一定的不方便。比如:
ThreadPool
不支持線程的取消、完成、失敗通知等交互性操作。ThreadPool
不支持線程執行的先後次序。
所以隨著 Task
類及其所提供的非同步編程模型的引入,Task
相較ThreadPool
具有更多的優勢。大概有一下幾點:
-
Task是.NET Framework的一部分,它提供了更高級別的抽象來表示非同步操作或併發任務。相比之下,ThreadPool較為底層,需要手動管理線程池和任務隊列。通過使用Task,我們可以以更簡潔、更可讀的方式表達併發邏輯,而無需關註底層線程管理的細節。
-
Task是基於Task Parallel Library(TPL)構建的核心組件,它提供了強大的非同步編程支持。利用Task,我們能夠輕鬆定義非同步方法、等待非同步操作完成以及處理任務結果。與此相反,ThreadPool主要用於執行委托或操作,缺乏直接的非同步編程功能。
-
Task在底層使用ThreadPool來執行任務,但它提供了更優秀的性能和資源管理機制。通過使用Task,我們可以利用TPL提供的任務調度器,智能化地管理線程池的大小、工作竊取演算法和任務優先順序。這樣一來,我們能夠更有效地利用系統資源,並獲得更好的性能表現。
-
Task擁有強大的任務關聯和組合功能。我們可以使用Task的
ContinueWith()
、When()
、WhenAll()
、Wait()
等方法定義任務之間的依賴關係,以及在不同任務完成後執行的操作。這種任務組合方式使併發編程更加靈活且易於管理。 -
Task提供了更好的異常處理和取消支持機制。我們可以利用Task的異常處理機制捕獲和處理任務中的異常,而不會導致整個應用程式崩潰。此外,Task還引入
CancellationToken
的概念,可用於取消任務的執行,從而更好地控制併發操作。
所以,儘管ThreadPool在某些情況下仍然有其用途,但在C#編程中,使用Task替代ThreadPool已變為通用實踐,推薦優先考慮使用Task來處理併發任務。
參考
編寫高質量代碼:改善C#程式的157個建議 / 陸敏技著.一北京:機械工業出版社,2011.9
作者: Niuery Daily
出處: https://www.cnblogs.com/pandefu/>
關於作者:.Net Framework,.Net Core ,WindowsForm,WPF ,控制項庫,多線程
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出 原文鏈接,否則保留追究法律責任的權利。 如有問題, 可郵件咨詢。