1. 介紹
想像執行緒是一個不知疲倦的員工,你把任務交給他。員工可能在睡覺(還沒開始工作)、拼命工作(正在執行你的方法)、等你下新任務(等待/閒置),或者完成工作(結束執行)。
在 C#(以及整體 .NET)中,執行緒的生命週期包含幾個狀態:
- Unstarted — 執行緒已建立,但還沒啟動。
- Running — 執行緒正在執行。
- WaitSleepJoin — 執行緒暫時不工作(例如等待訊號或「睡眠」)。
- Stopped — 執行緒已完成任務並結束。
可以把這個週期用下面這張圖表示:
stateDiagram-v2
[*] --> Unstarted
Unstarted --> Running: Start()
Running --> WaitSleepJoin: Wait/Sleep/Join
WaitSleepJoin --> Running: 收到訊號/時間到
Running --> Stopped: 方法結束
WaitSleepJoin --> Stopped: 方法結束
Stopped --> [*]
一切從建立 Thread 物件開始,但在你呼叫 Start() 之前,執行緒還在 Unstarted 狀態「打盹」。呼叫 Start() 後就開始運作,執行緒進入 Running。如果執行緒在程式內呼叫 Thread.Sleep 或等待某樣東西(例如 Monitor.Wait),它會進入等待的狀態。一旦傳入給執行緒的方法結束,執行緒就結束、消失,無法復活。結束就是結束,沒有回頭路。
2. 實作:簡單執行緒的生命週期
來看一個經典範例:
using System;
using System.Threading;
class Program
{
static void Main()
{
// 創建執行緒 — 目前只是安排工作
Thread worker = new Thread(DoWork);
Console.WriteLine($"執行緒建立後的狀態: {worker.ThreadState}");
// 啟動執行緒
worker.Start();
Console.WriteLine($"啟動後的執行緒狀態: {worker.ThreadState}");
// 讓主執行緒睡一下,給工作執行緒機會執行
Thread.Sleep(100);
Console.WriteLine($"稍後的執行緒狀態: {worker.ThreadState}");
// 等待 worker 完成 (join)
worker.Join();
Console.WriteLine($"結束後的執行緒狀態: {worker.ThreadState}");
Console.WriteLine("主執行緒已結束");
}
static void DoWork()
{
Console.WriteLine("工作執行緒開始工作!");
Thread.Sleep(500);
Console.WriteLine("工作執行緒完成工作!");
}
}
程式會輸出什麼?
- 建立後 — 狀態通常是 Unstarted。
- 啟動後 — 通常立刻是 Running(也可能看到 Running | Background)。
- 執行中 — 狀態可能是 Running,或如果執行緒在睡眠則是 WaitSleepJoin。
- 方法結束後 — 狀態變為 Stopped。
這段程式很適合理解執行緒可能的狀態。可以調整睡眠時間看看狀態如何變化。
3. 管理執行緒:主要方法
啟動: Start()
看似顯而易見,但重申一次:建立執行緒後要用 Start() 啟動。注意只能啟動一次:重複呼叫 Start() 會拋出 ThreadStateException。
Thread t = new Thread(MyMethod);
t.Start(); // 可以
t.Start(); // 錯誤!
等待完成: Join()
有時需要等執行緒把事做完再繼續,這時用 Join()。
Thread t = new Thread(MyMethod);
t.Start();
t.Join(); // 阻塞當前執行緒直到 t 結束
若有多個執行緒,可以對每個呼叫 Join() — 主執行緒會等所有「工人」完成。
變體:有個重載 Join(int millisecondsTimeout),只等待指定時間,時間到就繼續執行。
// 最多等 2 秒
if (t.Join(2000))
Console.WriteLine("執行緒準時完成");
else
Console.WriteLine("等到煩了...");
強制停止:為什麼這是壞主意
舊版 .NET 有個 Thread.Abort(),可以在任意時刻「殺掉」執行緒。現在這種做法幾乎不用了 —— 它危險,會讓程式處於不一致狀態。.NET 的哲學是:執行緒應該自願結束。你不是去「槍斃」員工,而是禮貌地告訴他下班了。
4. 如何正確地「停止」執行緒
最正確安全的停止方式是使用取消旗標或結束標誌,讓執行緒定期檢查它。
class Worker
{
private volatile bool shouldStop = false;
public void DoWork()
{
while (!shouldStop)
{
Console.WriteLine("我在工作!");
Thread.Sleep(300);
}
Console.WriteLine("執行緒收到停止命令,正在結束。");
}
public void RequestStop()
{
shouldStop = true;
}
}
使用方法:
Worker w = new Worker();
Thread t = new Thread(w.DoWork);
t.Start();
// 等一會兒
Thread.Sleep(1000);
// 請執行緒結束
w.RequestStop();
t.Join(); // 等待執行緒結束
重點: volatile
關鍵字 volatile 告訴編譯器和 CPU:「不要快取這個欄位,總是讀最新值!」這很重要,讓執行緒能看到外部對旗標的變更。沒有它(或其他同步機制),執行緒可能永遠看不到你的變更。
5. 執行緒進入等待與睡眠狀態
有時執行緒暫時不做事——要麼在等、要麼在睡。
睡眠: Thread.Sleep
當你想讓執行緒休息或降低執行速度(例如不要把 CPU 打滿),用 Thread.Sleep(milliseconds)。
// 執行緒睡 2 秒
Thread.Sleep(2000);
睡眠期間執行緒不做任何工作。
等待 / Join
當主執行緒等子執行緒結束(用 Join)時,主執行緒處於暫停狀態。同理,若執行緒在等待資源(例如 lock/Monitor 或其他同步原語),它也會進入等待狀態。
6. 管理執行緒的背景屬性
在 .NET 中執行緒分兩種:foreground(前景)和 background(背景)。差別很簡單:
- 如果進程只剩背景執行緒,進程會自動結束。
- 主執行緒和所有前景執行緒都結束後,進程才會停止。
可以明確將執行緒設為背景執行緒:
Thread t = new Thread(SomeMethod);
t.IsBackground = true; // 設為背景
t.Start();
實際範例 — 守護 vs 一般執行緒
Thread t = new Thread(() =>
{
while (true)
{
Console.WriteLine("我是幽靈(背景)執行緒,理論上不會被主動停止!");
Thread.Sleep(500);
}
});
t.IsBackground = true; // 設為背景
t.Start();
Thread.Sleep(1200);
Console.WriteLine("主執行緒結束");
// 當 Main 結束後,進程會結束,我們的無限循環執行緒也會隨之消失
Main 結束後進程結束;背景執行緒會自動終止。
7. 有用的小技巧
不該對執行緒做的事
- 不要嘗試「重啟」執行緒。 Thread 物件只能跑一次:方法一旦結束,執行緒就死了,重呼叫 Start() 會拋錯。
- 不要用危險的方法強制停止別人的執行緒(像 Thread.Abort() 或 Thread.Suspend())— 已過時而且危險。
- 不要忽略執行緒結束時的資源釋放。 若執行緒操作檔案或其他資源,務必在結束前妥善關閉。
檢查狀態與生命週期管理
if (t.IsAlive)
{
Console.WriteLine("執行緒仍然活著");
}
else
{
Console.WriteLine("執行緒已結束");
}
IsAlive 在執行緒還在執行方法時為 true;方法結束後為 false。
.NET 中簡單執行緒的生命週期
| 狀態 | 如何進入 | 代表什麼? | 如何離開 |
|---|---|---|---|
| Unstarted | |
執行緒已建立,尚未啟動 | 呼叫 Start() |
| Running | |
執行緒正在工作 | 方法結束 |
| WaitSleepJoin | Sleep(), Join(), 等待 | 執行緒暫時不活躍 | 等待結束 |
| Stopped | 執行緒方法已結束 | 執行緒「死亡」 | 不能離開 — 完結 |
常見問答(FAQ)關於生命週期管理
問題:能不能強制殺掉執行緒?
回答:不能也不該,執行緒應自行處理結束邏輯。用取消旗標來結束。
問題:能再次使用同一個 Thread 物件嗎?
回答:不能。需要新的工作就建立新的物件。
問題:如果主執行緒結束,但子執行緒還在跑會怎樣?
回答:如果子執行緒是背景的(IsBackground == true),應用會終止;如果不是,進程會等所有前景執行緒結束才停止。
問題:執行緒因取消而結束時,如何正確釋放資源?
回答:在執行緒方法內使用 try...finally,確保資源無論如何都會被釋放。
8. 常見錯誤與避免方法
錯誤 #1:重複使用同一個 Thread 物件。
不能對同一個執行緒物件呼叫多次 Start。執行緒結束後無法重啟,會拋例外。
錯誤 #2:在執行緒中未正確釋放外部資源。
若執行緒操作檔案、網路或其他資源,務必確保正確關閉與釋放。推薦使用 finally 或 using 來避免資源遺漏或鎖死。
錯誤 #3:建立過多執行緒。
太多執行緒會讓除錯困難,還可能降低效能。額外的執行緒意味著更多難追的 bug。
GO TO FULL VERSION