執行緒是作業系統分配 CPU 時間的基本單位。應用程式真正被排程、被暫停、被喚醒的對象,不是方法,也不是物件,而是執行緒。

這篇先整理執行緒本身的結構、排程方式,以及在 .NET 中最常碰到的前景執行緒與背景執行緒差異。

為什麼程式需要執行緒

一個行程可以包含多個執行緒。每個執行緒都代表一條獨立的執行路徑,可以同時處理不同工作,例如:

  • UI 執行緒負責回應使用者操作
  • 背景工作執行緒處理 I/O、計算或輪詢
  • GC、finalizer 等 runtime 工作也會使用自己的執行緒

從作業系統的角度看,每個執行緒至少會對應到幾個重要部分:

  • 核心物件:記錄排程狀態、優先權、CPU 暫存器內容等資訊
  • 使用者模式堆疊:保存區域變數、參數、返回位址
  • 執行環境資料:像是 TLS、例外處理鏈結、部分執行時狀態

也就是說,建立一條執行緒不是免費的。它會消耗記憶體,還會增加排程與切換成本。

上下文切換是什麼

CPU 在某個瞬間只會真正執行一條執行緒。當系統決定換人執行時,就會發生上下文切換(context switch)。

一次切換通常包含:

  1. 保存目前執行緒的 CPU 暫存器與執行狀態
  2. 從可執行的執行緒集合中挑選下一條
  3. 載入下一條執行緒的上下文
  4. 從新執行緒上次停下的位置繼續跑

這件事的問題不在於「能不能切」,而在於「切一次就有成本」。

常見成本包含:

  • 核心態切換本身需要時間
  • CPU cache 內原本對前一條執行緒有利的資料,可能立刻失效
  • 執行緒越多,排程器越難維持穩定吞吐

如果程式建立了大量長時間閒置或互相競爭的執行緒,最終往往不是更快,而是把 CPU 花在切換上。

Windows 如何排程執行緒

Windows 是搶佔式多工系統。意思是執行緒不需要自願讓出 CPU,排程器可以在適當時間直接中斷它,改去執行別條執行緒。

這種模型有幾個重要結果:

  • 某條執行緒不可能保證自己一直占用 CPU
  • 一條高優先權執行緒就緒時,低優先權執行緒可能立刻被搶走時間片
  • 寫死迴圈不會讓整台機器完全卡死,因為系統仍可切去其他執行緒

這也是為什麼多執行緒程式不能靠「我覺得應該先跑到這裡」來推理正確性。任何共享狀態,只要沒有同步機制,都應該視為不安全。

優先權如何影響排程

Windows 會為執行緒分配優先權,排程器傾向先執行優先權較高的執行緒。

在實務上可以把它理解成兩層:

  • 行程優先權類別(process priority class)
  • 執行緒相對優先權(relative thread priority)

兩者組合後,會形成排程器真正採用的基礎優先權。這也是為什麼同一個 Thread.Priority,放在不同 process priority class 下,效果可能不同。

優先權不是效能旋鈕,亂調通常會出問題:

  • 高優先權執行緒可能讓低優先權工作長期飢餓
  • Realtime 類別風險很高,甚至可能干擾系統本身的回應能力
  • 大部分業務程式應維持預設優先權,除非你非常清楚系統層面的代價

在 .NET 裡,最常見能直接碰到的是:

var thread = new Thread(DoWork);
thread.Priority = ThreadPriority.AboveNormal;
thread.Start();

這類設定應該非常保守。若只是希望程式「快一點」,更常見的正解不是改優先權,而是減少阻塞、降低共享競爭、避免多開無意義執行緒。

前景執行緒與背景執行緒

.NET 把執行緒分成前景執行緒(foreground thread)與背景執行緒(background thread)。

差異不在於排程方式,而在於程序結束時,CLR 會怎麼處理它們:

  • 只要還有前景執行緒活著,程序就不會結束
  • 當所有前景執行緒都結束後,CLR 會直接終止仍在執行的背景執行緒

這代表:

  • 前景執行緒適合真正必須做完的工作
  • 背景執行緒適合輔助性工作,例如快取清理、非關鍵輪詢、遙測彙整

簡單範例:

var thread = new Thread(() =>
{
    while (true)
    {
        Thread.Sleep(1000);
        Console.WriteLine("running...");
    }
});

thread.IsBackground = true;
thread.Start();

如果主執行緒結束,這條背景執行緒不會阻止程序退出。

另外幾點也值得記住:

  • 透過 new Thread(...) 建立的執行緒預設是前景執行緒
  • 執行緒池中的工作執行緒預設是背景執行緒
  • 是否設成背景,不會讓它比較省資源,只影響程序收尾行為

建立執行緒的成本

手動建立執行緒適合少量、長生命週期、行為明確的工作;如果只是想把一小段工作丟到背景跑,通常不該直接 new Thread(...)

原因很直接:

  • 每條執行緒都有自己的堆疊與核心物件
  • 建立與銷毀都有成本
  • 執行緒數量上升後,上下文切換與 GC 暫停協調都會變差

這也是後面會引出 ThreadPool 與 Task 的原因:它們的核心目標,就是減少「為了一小段工作就新建執行緒」這種浪費。

實務上該記住什麼

  • 執行緒是昂貴資源,不要把它當一般物件大量建立
  • 多執行緒不代表一定更快,先看工作是否真的能並行
  • 排程順序不可預測,不要依賴碰運氣的時序
  • 優先權設定通常不是優化手段,而是最後才碰的系統層選項
  • 背景執行緒會在程序收尾時被直接中止,不能拿來承載必須完成的任務

參考資料

  • 《CLR via C#》第四版,Jeffrey Richter