CodeGym /課程 /C# SELF /執行緒的生命週期與管理

執行緒的生命週期與管理

C# SELF
等級 55 , 課堂 2
開放

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("工作執行緒完成工作!");
    }
}

程式會輸出什麼?

  1. 建立後 — 狀態通常是 Unstarted
  2. 啟動後 — 通常立刻是 Running(也可能看到 Running | Background)。
  3. 執行中 — 狀態可能是 Running,或如果執行緒在睡眠則是 WaitSleepJoin
  4. 方法結束後 — 狀態變為 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
new Thread(...)
執行緒已建立,尚未啟動 呼叫 Start()
Running
Start()
執行緒正在工作 方法結束
WaitSleepJoin Sleep(), Join(), 等待 執行緒暫時不活躍 等待結束
Stopped 執行緒方法已結束 執行緒「死亡」 不能離開 — 完結

常見問答(FAQ)關於生命週期管理

問題:能不能強制殺掉執行緒?
回答:不能也不該,執行緒應自行處理結束邏輯。用取消旗標來結束。

問題:能再次使用同一個 Thread 物件嗎?
回答:不能。需要新的工作就建立新的物件。

問題:如果主執行緒結束,但子執行緒還在跑會怎樣?
回答:如果子執行緒是背景的(IsBackground == true),應用會終止;如果不是,進程會等所有前景執行緒結束才停止。

問題:執行緒因取消而結束時,如何正確釋放資源?
回答:在執行緒方法內使用 try...finally,確保資源無論如何都會被釋放。

8. 常見錯誤與避免方法

錯誤 #1:重複使用同一個 Thread 物件。
不能對同一個執行緒物件呼叫多次 Start。執行緒結束後無法重啟,會拋例外。

錯誤 #2:在執行緒中未正確釋放外部資源。
若執行緒操作檔案、網路或其他資源,務必確保正確關閉與釋放。推薦使用 finallyusing 來避免資源遺漏或鎖死。

錯誤 #3:建立過多執行緒。
太多執行緒會讓除錯困難,還可能降低效能。額外的執行緒意味著更多難追的 bug。

2
任務
C# SELF, 等級 55, 課堂 2
上鎖
執行緒的主要狀態
執行緒的主要狀態
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION