天天看點

并發程式設計 ---為何要線程池化

作者:opendotnet

引言

衆所周知,使用線程可以極大的提高應用程式的效率和響應性,提高使用者體驗,但是不可以無節制的使用線程,為什麼呢?

線程的開銷

線程的開銷實際上是非常大的,我們從空間開銷和時間開銷上分别讨論。

線程的空間開銷

線程的空間開銷來自這四個部分:

  1. 線程核心對象(Thread Kernel Object)。每個線程都會建立一個這樣的對象,它主要包含線程上下文資訊,在32位系統中,它所占用的記憶體在700位元組左右。
  2. 線程環境塊(Thread Environment Block)。TEB包括線程的異常處理鍊,32位系統中占用4KB記憶體。
  3. 使用者模式棧(User Mode Stack),即線程棧。線程棧用于儲存方法的參數、局部變量和傳回值。每個線程棧占用1024KB的記憶體。要用完這些記憶體很簡單,寫一個不能結束的遞歸方法,讓方法參數和傳回值不停地消耗記憶體,很快就會發生

    OutOfMemoryException

  4. 核心模式棧(Kernel Mode Stack)。當調用作業系統的核心模式函數時,系統會将函數參數從使用者模式棧複制到核心模式棧。在32位系統中,核心模式棧會占用12KB記憶體。

線程的時間開銷

線程的時間開銷來自這三個過程:

  1. 線程建立的時候,系統相繼初始化以上這些記憶體空間。
  2. 接着CLR會調用所有加載DLL的DLLMain方法,并傳遞連接配接标志(線程終止的時候,也會調用DLL的DLLMain方法,并傳遞分離标志)。
  3. 線程上下文切換。一個系統中會加載很多的程序,而一個程序又包含若幹個線程。但是一個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 != )
 {
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

具有更多的優勢。大概有一下幾點:

  1. Task是.NET Framework的一部分,它提供了更進階别的抽象來表示異步操作或并發任務。相比之下,ThreadPool較為底層,需要手動管理線程池和任務隊列。通過使用Task,我們可以以更簡潔、更可讀的方式表達并發邏輯,而無需關注底層線程管理的細節。
  2. Task是基于Task Parallel Library(TPL)建構的核心元件,它提供了強大的異步程式設計支援。利用Task,我們能夠輕松定義異步方法、等待異步操作完成以及處理任務結果。與此相反,ThreadPool主要用于執行委托或操作,缺乏直接的異步程式設計功能。
  3. Task在底層使用ThreadPool來執行任務,但它提供了更優秀的性能和資源管理機制。通過使用Task,我們可以利用TPL提供的任務排程器,智能化地管理線程池的大小、工作竊取算法和任務優先級。這樣一來,我們能夠更有效地利用系統資源,并獲得更好的性能表現。
  4. Task擁有強大的任務關聯群組合功能。我們可以使用Task的

    ContinueWith()

    When()

    WhenAll()

    Wait()

    等方法定義任務之間的依賴關系,以及在不同任務完成後執行的操作。這種任務組合方式使并發程式設計更加靈活且易于管理。
  5. Task提供了更好的異常處理和取消支援機制。我們可以利用Task的異常處理機制捕獲和處理任務中的異常,而不會導緻整個應用程式崩潰。此外,Task還引入

    CancellationToken

    的概念,可用于取消任務的執行,進而更好地控制并發操作。

是以,盡管ThreadPool在某些情況下仍然有其用途,但在C#程式設計中,使用Task替代ThreadPool已變為通用實踐,推薦優先考慮使用Task來處理并發任務。

以上部分内容引用自

《編寫高品質代碼:改善C#程式的157個建議》 / 陸敏技著.一北京:機械工業出版社,2011.9

歡迎關注我的個人部落格:www.niuery.com

繼續閱讀