1. 介紹
如果把一個 process 想像成超級市場,執行緒(thread)就是不同收銀台的收銀員,他們同時服務顧客。所有收銀員都在同一家店裡工作,但每個人並行執行自己的任務,這樣整體工作會更快更有效率。執行緒讓一個 process 裡能同時做多件事,分享共同資源並協調應用程式內部的工作。最重要的優點是:如果 CPU 支援多工,執行緒可以真正在平行上運作。
實務上需要執行緒的情況
為什麼我們要啟動多個執行緒?下面是幾個常見情境:
- 你在寫一個有 GUI 的應用程式,不希望在長時間操作時介面「當掉」。
- 你需要同時下載多個檔案。
- 在遊戲中,敵人應該獨立思考,不受其他邏輯影響。
很驚訝的是,許多程式仍然會因為不當使用執行緒而「當機」。今天我們會學到如何避免這類問題。
類別 Thread:手動多執行緒的基礎
類別 Thread 是 .NET 多執行緒編程中的老古董。雖然有更新穎的工具(Task、async/await)出現,但學習用 Thread 操作仍然有價值,特別是如果你想從零開始理解執行緒的運作。
建立執行緒的流程
- 建立一個 Thread 物件,並傳入要在執行緒中執行的方法。
- 使用 Start() 啟動執行緒。
- (可選)觀察發生了什麼 — 因為一切可能是同時發生的!
2. 使用 Thread 啟動執行緒
我們來實際體驗平行處理的魔力。加一個小類別到範例應用裡,負責數到某個數字並顯示進度。我們會把這個工作放到獨立執行緒去執行。
using System;
using System.Threading;
class Program
{
static void Main()
{
Console.WriteLine("主執行緒已啟動!");
// 建立執行緒物件,指定要執行的方法
Thread workerThread = new Thread(CountToTen);
// 啟動新執行緒
workerThread.Start();
// 主執行緒也做點事:印出點...
for (int i = 0; i < 5; i++)
{
Console.Write(".");
Thread.Sleep(500); // 為了好看而暫停
}
Console.WriteLine("\n主執行緒結束!");
}
static void CountToTen()
{
for (int i = 1; i <= 10; i++)
{
Console.WriteLine($"[執行緒] 計數: {i}");
Thread.Sleep(400);
}
Console.WriteLine("[執行緒] 完成!");
}
}
發生了什麼事?
你會在主控台看到點和 "計數: X" 交錯出現。這是多執行緒的第一個跡象!主執行緒印點,而新執行緒計數到 10。他們互不干擾,就像樂團裡的兩位音樂家:一個打鼓,另一個彈鋼琴,各自表現,合起來就是音樂。
3. 如何向執行緒傳遞資料?
有時執行緒不只需要知道要做什麼,還要知道要作用的資料。如果執行緒方法需要參數,該怎麼傳進去?
方式一:使用 lambda(匿名方法)
int bounds = 7;
Thread t = new Thread(() => CountToNumber(bounds));
t.Start();
static void CountToNumber(int n)
{
for (int i = 1; i <= n; i++)
{
Console.WriteLine($"[執行緒] {i} / {n}");
Thread.Sleep(300);
}
}
這裡我們把對目標方法的呼叫包在 lambda 裡,以便傳入參數。這種做法很常見,因為 Thread 期望的是無參數的方法(ThreadStart)。
方式二:使用 ParameterizedThreadStart
也可以用專門的 delegate ParameterizedThreadStart,它接受一個型別為 object 的參數。
Thread t = new Thread(CountToNumberObject);
t.Start(12);
static void CountToNumberObject(object? n)
{
int max = (int)n!;
for (int i = 1; i <= max; i++)
{
Console.WriteLine($"[執行緒] {i} / {max}");
Thread.Sleep(200);
}
}
是的,參數型別是 object,因此需要做型別轉換。這不太理想,但能運作。現代 C# 通常偏好 lambda 的方式。
4. 管理執行緒的生命
我們來看看 Thread 提供了哪些好用的東西。
| 屬性 / 方法 | 用途 |
|---|---|
|
啟動執行緒(執行建立時指定的方法) |
|
等待執行緒結束(阻塞呼叫執行緒直到另一個執行緒完成) |
|
檢查執行緒當前是否在運行(true/false) |
|
給執行緒命名(對除錯有幫助) |
|
取得代表當前執行緒的物件 |
|
讓當前執行緒暫停 ms 毫秒 |
範例:等待執行緒結束
有時候需要讓主執行緒等候輔助執行緒完成工作。
Thread t = new Thread(() =>
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"[第二執行緒] {i}");
Thread.Sleep(300);
}
});
t.Start();
Console.WriteLine("[主執行緒] 等待第二執行緒結束...");
t.Join(); // 主執行緒在這裡等
Console.WriteLine("[主執行緒] 第二執行緒已結束!");
沒有 Join() 的話,程式可能在執行緒仍在工作時就結束。有了 Join(),主執行緒會耐心等待所有工作完成。
5. 有用的小技巧
給執行緒命名:避免混亂
為了除錯可以給執行緒設名:
Thread t = new Thread(() =>
{
Console.WriteLine($"這在執行緒執行: {Thread.CurrentThread.Name}");
});
t.Name = "計數器-執行緒";
t.Start();
當執行緒變多,每個做不同工作時,這會很有幫助。
限制與實務上的趨勢
要說明的是:在現代應用中,透過 Thread 手動管理執行緒已不常見。實務上常用更強大、更智慧的工具(Task、async/await),我們之後會詳細學。但理解執行緒基礎很重要,因為它有助於:
- 理解 C# 和 .NET 的「內幕」。
- 面試時(有時會要求你解釋 Thread 與 Task 的差別)。
- 診斷與修復大型遺留專案中的複雜問題。
總結圖:執行緒的生命週期
stateDiagram-v2
[*] --> New: Thread 已建立
New --> Running: Start()
Running --> Stopped: 方法完成
Stopped --> [*]
現在你已經會自己建立並啟動 C# 的執行緒。你不再只是火車裡的乘客,而是能同時駕駛好幾節車廂的司機!接下來會學到執行緒的生命週期管理、同步以及平行化的新領域。
6. 使用 Thread 時常見錯誤與技巧
錯誤 #1:忘記啟動執行緒。
常見情況是建立了 Thread 物件,卻忘了呼叫 Start()。結果執行緒沒開始跑,問題不容易立刻發現。
錯誤 #2:在沒有同步機制下修改共享資料。
如果多個執行緒同時修改同一個變數卻沒保護,就會出問題!就像兩個收銀員從同一個錢箱拿找零——很快就會亂成一團。
錯誤 #3:使用已過時或不安全的方法。
不要使用 Thread.Suspend()、Thread.Resume() 之類的方法——它們被視為危險且過時。用其他方式管理執行緒生命週期。
錯誤 #4:執行緒內未處理的例外。
如果執行緒內發生例外且未被攔截,該執行緒會終止,而主執行緒可能沒發現!把執行緒的程式碼包在 try-catch 中來捕捉並記錄錯誤。
Thread t = new Thread(() =>
{
try
{
// ... 你的程式碼
}
catch (Exception ex)
{
// 記錄錯誤
Console.WriteLine($"[執行緒] 錯誤: {ex.Message}");
}
});
t.Start();
GO TO FULL VERSION