並行程式設計 ---為何要執行緒池化

2023-07-18 21:00:28

引言

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

執行緒的開銷

執行緒的開銷實際上是非常大的,我們從空間開銷和時間開銷上分別討論。

執行緒的空間開銷

執行緒的空間開銷來自這四個部分:

  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根據自身的演演算法來做這個決定。

執行緒池技術能讓我們重點關注業務的實現,而不是執行緒的效能測試。

微軟除實現了執行緒池外,還需要關注一個型別:BackgroundWorkerBackgroundWorker 是在內部使用了執行緒池的技術:同時,在WinForm或WPF編碼中,它還給工作執行緒和UI執行緒提供了互動的能力。

實際上, ThreadThreadPool 預設都沒有提供這種互動能力,而 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具有更多的優勢。大概有一下幾點:

  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