1. 介紹
多執行緒就像辦公室裡幾個人同時做事:一個在印文件、另一個在打電話、第三個在煮咖啡(當然,他們都是程式設計師)。如果只有一個人,他會把所有事情排隊做,辦公室會因為等咖啡而悶到窒息。在程式裡也是一樣:單執行緒程式一次只能做一件事。
想像你的應用在做一個很耗時的操作,比如從網路下載檔案或計算超大的表格。其他東西在那段時間會「凍結」——按鈕按不下去、動畫不動,還會跳出「沒有回應」之類的提示視窗。
多執行緒讓應用能同時做好幾件事:界面保持回應、操作可以並行執行,這樣你就不用對著電腦喊「電腦,你又死當了?!」。
基本概念與術語
在潛水之前,先搞清楚什麼是執行緒(thread)以及它和進程的差別。
- 進程 (Process): 獨立的程式,有自己的地址空間、變數和資源。例如在 Windows 裡每個被啟動的應用通常是獨立的進程。
- 執行緒 (Thread): 在進程內的執行單位。進程可以包含一或多個執行緒,這些執行緒會共同使用同一份資源(記憶體、變數)。
為什麼多執行緒會引發那麼多疑問?
因為執行緒是愛冒險的傢伙:它們可能在任何時刻開始執行、把資料搞亂、互相打斷,若不控制好順序,記憶體就會變成災難現場。如果你覺得這像沒大人帶的托兒所——完全沒錯!對執行緒的紀律和注意是寫出可靠多執行緒程式的基礎。
2. C# 與 .NET 中多執行緒的歷史與角色
早期 C# 程式比較單純、也常是單執行緒的。隨著效能要求增加、多核 CPU 的普及,以及需要做出不會卡住介面的應用,.NET 加入了多執行緒的工具。最開始有經典的 System.Threading.Thread 類別,後來出現了 tasks(Task)、非同步方法(async/await)、資料平行處理(PLINQ)以及更多高階的同步原語。
C# 發展成一個強大的平台,在那裡多執行緒不是稀罕玩意,而是日常。
圖示:進程與執行緒
來一個簡單的示意圖:
+--------------------------------------------------+
| 進程 (你的程式) |
| +-------------+ +-------------+ |
| | 執行緒 1 | | 執行緒 2 | |
| +-------------+ +-------------+ |
| ... |
| +-------------+ |
| | 執行緒 N | |
| +-------------+ |
+--------------------------------------------------+
同一個進程裡的所有執行緒都能看到共同的變數和資源。
3. 如何在 C# 中建立執行緒?
從最基礎開始:Thread 類別,屬於命名空間 System.Threading。
範例:啟動第二個執行緒
假設有一個耗時任務——例如計算從 1 到 10_000_000 的總和。當計算在跑時,主執行緒可以印出對使用者的問候。
using System;
using System.Threading;
class Program
{
// 第二個任務的方法
static void CalculateSum()
{
long sum = 0;
for (int i = 1; i <= 10_000_000; i++)
sum += i;
Console.WriteLine($"[執行緒 2] 總和: {sum}");
}
static void Main()
{
// 建立執行緒,傳入方法的 delegate
Thread thread = new Thread(CalculateSum);
thread.Start(); // 啟動第二個執行緒
// 主執行緒繼續工作
Console.WriteLine("[執行緒 1] 嗨!我們在並行工作...");
// 在退出前等第二個執行緒完成
thread.Join();
Console.WriteLine("[執行緒 1] 全部完成!");
}
}
會發生什麼?
畫面會先出現 "[執行緒 1] 嗨!我們在並行工作...",然後當計算執行緒結束時會輸出總和。
典型問題:誰先輸出
多試幾次執行這段程式——輸出順序可能會不一樣!有時總和先出、有時問候先出。這就是多執行緒的真實面貌——程式變得不那麼可預測,就像星期一貓的心情。
4. 記憶體區域:執行緒能看見什麼?
同一個進程裡的執行緒可以存取相同的變數(除非變數是方法的區域變數)。如果一個執行緒改變變數,其他執行緒也會看到變化!
範例:共享變數
using System;
using System.Threading;
class Program
{
static int counter = 0;
static void Increment()
{
for (int i = 0; i < 1000; i++)
counter++;
}
static void Main()
{
Thread t1 = new Thread(Increment);
Thread t2 = new Thread(Increment);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"counter = {counter}");
}
}
我們預期在 counter 看到多少?照理每個執行緒會做 1000 次遞增,應該是 2000。
結果不是這樣!多跑幾次會看到不同的數值:1782、1935、1999…
為什麼?這就是經典的 Race Condition ——執行緒在讀取 -> 增加 -> 寫入 的間隙互相搶份,部分遞增被丟失了。
5. 有用的小細節
執行緒如何和 UI 互動?
在現代的桌面應用(WinForms/WPF/MAUI)裡,主執行緒通常負責處理圖形介面。所有使用者事件(點擊、輸入)都在主執行緒處理。背景工作應放到其他執行緒,但依規則不能直接從背景執行緒去「碰」UI,以免造成混亂。
在主控台程式沒這個限制,可以從任何執行緒呼叫 Console.WriteLine。不過在真實應用中,若沒正確同步,介面還是會出問題。
順序與並行
用表格幫你記住差別。
| 單執行緒程式 | 多執行緒程式 |
|---|---|
| 依序執行任務 | 任務可能同時執行 |
| 長時間任務會讓 UI 卡住 | UI 保持回應 |
| 讀寫變數很簡單 | 需要控制對資料的存取 |
| 比較容易除錯 | 除錯可能很麻煩 |
使用執行緒時的重要事項
- 共享變數 = 共享風險。 如上例,如果多個執行緒使用同一個變數而不做同步,就會出錯!(後面的課會詳談)
- 可以用 Join() 等待執行緒。 Join() 會把呼叫它的執行緒暫停,直到目標執行緒結束。如果你需要等結果就用它。
- Thread 不能重複啟動。 執行緒結束後不能再用同一個物件啟動——要新建一個 Thread。
- 結束執行緒。 執行緒會在其方法執行完畢時終止。強制「殺死」執行緒通常是壞主意(像 Abort() 這種方法已被認為有害且過時)。
多執行緒在實務上的用途
- UI 應用:避免在背景載入或計算時凍結介面。
- 伺服器與服務:同時處理大量客戶端的請求。
- 高效能計算:把大任務(例如處理上百萬筆資料)切成小塊並行處理。
- 遊戲、模擬、資料處理:模擬複雜系統同時保持效能。
多執行緒的問題
多執行緒給你力量,也帶來麻煩:
- Race Condition (狀態競賽): 當多個執行緒同時修改相同資料,結果會依賴指令的執行順序,這個順序是不可預測的。
- Deadlock(互相鎖定): 執行緒互相等待對方,沒有人能繼續執行。
- Starvation(飢餓): 某個執行緒持續得不到資源,永遠被剝奪執行機會。
這堂課我們只提到多執行緒的主要問題,接下來的課會教你如何識別與避免它們。現在只要記住:如果程式「凍住」了,造成問題的不一定只是 bug,可能是執行緒在那邊開派對。
GO TO FULL VERSION