執行緒是作業系統分配 CPU 時間的基本單位。應用程式真正被排程、被暫停、被喚醒的對象,不是方法,也不是物件,而是執行緒。
這篇先整理執行緒本身的結構、排程方式,以及在 .NET 中最常碰到的前景執行緒與背景執行緒差異。
為什麼程式需要執行緒
一個行程可以包含多個執行緒。每個執行緒都代表一條獨立的執行路徑,可以同時處理不同工作,例如:
- UI 執行緒負責回應使用者操作
- 背景工作執行緒處理 I/O、計算或輪詢
- GC、finalizer 等 runtime 工作也會使用自己的執行緒
從作業系統的角度看,每個執行緒至少會對應到幾個重要部分:
- 核心物件:記錄排程狀態、優先權、CPU 暫存器內容等資訊
- 使用者模式堆疊:保存區域變數、參數、返回位址
- 執行環境資料:像是 TLS、例外處理鏈結、部分執行時狀態
也就是說,建立一條執行緒不是免費的。它會消耗記憶體,還會增加排程與切換成本。
上下文切換是什麼
CPU 在某個瞬間只會真正執行一條執行緒。當系統決定換人執行時,就會發生上下文切換(context switch)。
一次切換通常包含:
- 保存目前執行緒的 CPU 暫存器與執行狀態
- 從可執行的執行緒集合中挑選下一條
- 載入下一條執行緒的上下文
- 從新執行緒上次停下的位置繼續跑
這件事的問題不在於「能不能切」,而在於「切一次就有成本」。
常見成本包含:
- 核心態切換本身需要時間
- 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