1. 介紹
當我們使用 Task 類別或非同步方法(async/await)時,可以用習慣的方式捕捉例外——在 await 外層用 try-catch,或用 ContinueWith。例外不會「消失」,會回到呼叫執行緒。
但如果用 Thread 建立執行緒,就麻煩了。每個執行緒有自己的入口點(ThreadStart)和執行上下文。如果執行緒內發生未處理的例外,它不會「回到」主執行緒——例外只會在該執行緒中拋出。
- 在 .NET Framework:某個執行緒的未處理例外會終止整個應用程式。
- 在 .NET (Core/5+):只有該執行緒會結束,應用程式會繼續運作(這可能導致隱藏的 bug)。
結論:如果不在執行緒內捕捉例外,你很可能看不到它們。因此在執行緒內做健全的錯誤處理是必要的。
有趣的事實:從 Thread 飛出去的例外就像隱形忍者:消失了,之後你還會想不透為何邏輯沒跑起來。
2. 執行緒內例外怎麼運作?
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread thread = new Thread(DoWork);
thread.Start();
// 等待執行緒結束,好看到底會發生什麼
thread.Join();
Console.WriteLine("Main 正常結束");
}
static void DoWork()
{
Console.WriteLine("在獨立執行緒工作...");
throw new Exception("出事了!執行緒裡發生錯誤。");
}
}
根據平台(舊的 .NET Framework 或新的 .NET Core/5/6/7/8/9),行為不同:要嘛整個應用崩潰,要嘛只有執行緒崩潰。但重點是——例外不會飛到主執行緒,你無法在外面處理它。
重要!試圖把 thread.Join() 用 try-catch 包起來,不能捕捉另一執行緒裡的例外——那例外「活在」它自己的執行緒裡頭。
3. 在 Thread 中該怎麼捕捉例外?
只能在執行緒內捕捉——也就是你傳給 Thread 構造子的那個函式。把所有可能拋錯的地方用 try-catch 包起來。
static void DoWork()
{
try
{
Console.WriteLine("工作中...");
throw new Exception("又出問題了!");
}
catch (Exception ex)
{
Console.WriteLine($"[執行緒] 捕到例外: {ex.Message}");
// 這裡可以記錄、發到 UI/伺服器等
}
}
執行緒裡的錯誤處理是該執行緒程式碼的責任。不能指望呼叫方自動把錯誤捕住。
4. 如何在主執行緒知道另一個執行緒出問題了?
在真實應用中,把錯誤資訊傳回主執行緒很重要。
- 使用線程安全的機制,例如 ConcurrentQueue<Exception>,把執行緒的例外傳遞出來。
- 在工作執行緒中觸發事件/委託,把錯誤告知上層。
- 優先使用 Task,它能把例外「自帶」傳到你 await 的地方。
示例:把錯誤收集到一個指定位置
using System;
using System.Threading;
class Program
{
static Exception? threadException = null;
static void Main()
{
Thread thread = new Thread(DoWork);
thread.Start();
thread.Join();
if (threadException != null)
{
Console.WriteLine($"另一個執行緒發生錯誤: {threadException.Message}");
}
else
{
Console.WriteLine("執行緒沒有錯誤結束。");
}
}
static void DoWork()
{
try
{
throw new Exception("另一個執行緒出事了!");
}
catch (Exception ex)
{
threadException = ex;
}
}
}
備註:這種做法適合同步等待(Join())。如果執行緒「持續活著」或錯誤很多,請改用 ConcurrentQueue<Exception>、事件或其他通訊機制。
5. 與 Task 的比較:為什麼錯誤處理更簡單
async Task FooAsync()
{
throw new Exception("Task 裡的錯誤!");
}
try
{
await FooAsync();
}
catch (Exception ex)
{
Console.WriteLine($"捕到錯誤: {ex.Message}");
}
這裡一切透明:錯誤會傳到你 await 的地方。用經典的 Thread 時,錯誤留在執行緒內,不會自動傳上來。這也是為何傾向用 Task 和現代抽象的原因之一。
6. 實際範例
在 UI 應用(WPF/WinForms)中,會用執行緒避免阻塞介面。沒處理的例外會造成「灰畫面」或奇怪的當掉。
糟的範例(執行緒沒處理錯誤)
Thread thread = new Thread(() =>
{
// 想很久
Thread.Sleep(5000);
throw new Exception("完了!"); // 沒人會捕捉到
});
thread.Start();
好的範例(捕捉錯誤並通知使用者)
Thread thread = new Thread(() =>
{
try
{
Thread.Sleep(5000);
throw new Exception("哪裡不對勁");
}
catch (Exception ex)
{
// 可以顯示 MessageBox、記錄或把資訊轉給 UI
Console.WriteLine($"執行緒錯誤: {ex.Message}");
}
});
thread.Start();
7. 有用的小技巧
全域勾子處理執行緒的未處理例外
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
Console.WriteLine($"全域捕到例外: {((Exception)args.ExceptionObject).Message}");
};
Thread thread = new Thread(() =>
{
throw new Exception("Exterminate!");
});
thread.Start();
處理器 AppDomain.CurrentDomain.UnhandledException 會在執行緒未處理例外時觸發,但無法在 .NET Framework 中「復活」執行緒或防止程序終止。在 .NET (Core/5+) 它通常只會記錄錯誤;如果其他執行緒還在,應用可能會繼續運作。
例外處理差異 — Thread vs Task
|
|
|
|---|---|---|
| 在哪裡捕捉 | 在執行緒內 | 在呼叫端(await、ContinueWith 等) |
| 後果 | 例外會遺失/殺死執行緒(或在 .NET Framework 中整個應用) | 例外會傳到等待點(await) |
| 如何通知上層 | 只能明示(變數、事件、佇列) | 透過 await,同步等待時會有 AggregateException |
| 記錄 | 需在執行緒程式碼中手動記錄 | 通常在包住 await 的 try-catch 中處理 |
| 上下文 | 獨立於父執行緒 | Task 使用呼叫方的同步上下文(例如 WPF 的 UI context) |
8. 在 Thread 中處理例外的常見錯誤
錯誤 #1:在執行緒內不捕捉例外。
結果可能是應用部分無聲地結束,或有時整個過程崩潰,而且沒有明確的診斷資訊。
錯誤 #2:試圖在主執行緒「捕」執行緒內的例外。
這沒效:把 try-catch 放在 thread.Join() 或 thread.Start() 外面不會捕到執行緒裡拋出的錯誤。
錯誤 #3:丟失錯誤資訊。
如果執行緒崩了,而你沒有明確傳遞例外(變數、佇列、事件),你不知道原因也拿不到細節。這會導致「幽靈」bug。
錯誤 #4:沒有記錄。
一定要在執行緒中把錯誤記錄下來,即便看起來「沒什麼大不了」。
GO TO FULL VERSION