1. 什麼是 "fire and forget"?
在程式設計裡,fire and forget 指的是啟動一個任務但不等待它完成。在 C# 和 .NET 世界,通常用 Task 啟動任務,但不在任何地方等待(沒有 await)、不保留引用,實際上就把它忘了。
// 按鈕在背景啟動任務,但在任何地方都不 await。
button.Click += (s, e) =>
{
Task.Run(() => 長時間操作());
};
聽起來很吸引人:「讓它在背景跑,我先做我的事」。但用這種方式,如果任務內發生例外,沒有人會及時知道——它會靜悄悄地丟失。
2. Task 中的例外處理如何運作
經典:用 await 處理錯誤
處理非同步任務的標準方式是透過 await。如果任務裡發生錯誤,錯誤會在等待點拋出:
try
{
await SomeOperationAsync(); // 如果裡面有 Exception,會在這裡進到 catch
}
catch(Exception ex)
{
Console.WriteLine("哎呀!任務發生錯誤: " + ex.Message);
}
也就是說,當你等待任務完成時,就不會漏掉例外。
但 fire-and-forget 任務沒人等它!
public void 啟動不等待()
{
// 任務自己跑。沒人等它...
Task.Run(() => {
// 任務內某處出了問題:
throw new InvalidOperationException("哎呀,一切都完了!");
});
// 方法結束,任務在背景默默執行。
}
如果這種任務內發生例外,它不會在主執行緒拋出。程式會繼續運作,好像什麼事都沒發生。
重要
在 .NET 中,發生未處理例外的任務會進入 Faulted 狀態。但如果你不等待它(沒有 await、沒有 .Result、.Wait() 等),沒有人會讀取那個例外,也不會在呼叫端顯現。
底層實際發生了什麼?
對於沒人等待的任務,唯一剩下被注意到的機會就是事件 TaskScheduler.UnobservedTaskException。當 GC(垃圾回收)發現一個帶有未被觀察例外的任務物件時,它會觸發這個事件。但這不是即時發生,也不是在你預期的地方發生——不能依賴它。
3. 演示:Fire-and-forget 的錯誤
// 範例:從 Main 直接啟動 fire-and-forget 任務
using System;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
FireAndForgetExample();
Console.WriteLine("主執行緒繼續運作...");
// 給任務點時間完成
Task.Delay(2000).Wait();
}
static void FireAndForgetExample()
{
Task.Run(() =>
{
Console.WriteLine("Fire-and-forget 任務開始!");
Task.Delay(500).Wait();
throw new InvalidOperationException("fire-and-forget 任務內的錯誤!");
});
}
}
如果執行這段程式,通常……不會有什麼特別的事。錯誤會發生,但程式不會知道。有時在 IDE 的 Output Window 會看到警告,但對使用者來說——完全沒有訊息。
為什麼這在真實專案中很危險?
- 複雜的 bug 很難重現(「有時不工作——不知道為什麼」)。
- 靜默的資料或邏輯損失(例如沒有發送出客戶郵件)。
- 在 production 中如果沒設好日誌,根本收不到問題的訊號。
4. 在 fire-and-forget 中正確處理錯誤的方法
在任務內部記錄並處理錯誤
最基本的安全做法是在 fire-and-forget 任務內部捕獲例外:
Task.Run(() =>
{
try
{
// 你的長時間/危險程式碼
throw new InvalidOperationException("出了些問題!");
}
catch (Exception ex)
{
// 記錄錯誤或通知使用者
Console.WriteLine("Fire-and-forget: 捕捉到例外: " + ex.Message);
// 可以寫入 log 檔、使用監控/告警系統等。
}
});
非同步的 void 方法(以及為何不建議這麼做)
async void DangerousFireAndForget()
{
// 一些危險的事
throw new Exception("砰!");
}
async void 方法本質上就是 fire-and-forget:沒辦法等待它們,因為它們不回傳 Task。它們的例外會飛到全域的應用程式處理器(例如 AppDomain.UnhandledException),經常會導致程序崩潰。只在 event handler 使用 async void,而且要非常小心。
用輔助方法統一處理錯誤
把安全的 fire-and-forget 啟動封裝成一個 wrapper 很方便:
// 通用方法:安全地啟動 fire-and-forget
public static void RunSafeFireAndForget(Func<Task> taskFactory)
{
Task.Run(async () =>
{
try
{
await taskFactory();
}
catch (Exception ex)
{
// 記錄例外
Console.WriteLine("Fire-and-forget (safe): " + ex);
// 可以把錯誤發送到監控系統!
}
});
}
// 使用範例:
RunSafeFireAndForget(async () =>
{
await Task.Delay(1000);
throw new InvalidOperationException("在 fire-and-forget 裡面!");
});
真實場景範例:發送 email
// 發送郵件按鈕:
private void buttonSend_Click(object sender, EventArgs e)
{
Task.Run(() => SendEmail());
}
// 發送方法:
private void SendEmail()
{
try
{
// 這裡可能是真正的發送邏輯
throw new Exception("無法連到 SMTP 伺服器!");
}
catch (Exception ex)
{
// 記錄
File.AppendAllText("errors.log", $"發送錯誤: {ex.Message}\n");
}
}
5. 那 UnobservedTaskException 怎麼辦?
作為最後手段,.NET 提供了事件 TaskScheduler.UnobservedTaskException。如果任務以錯誤結束、沒有人等待它,且該任務物件被 GC 回收,就會觸發這個事件。不能依賴它——這是「最後一線希望」機制。
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
Console.WriteLine("全域 UnobservedTaskException: " + e.Exception);
e.SetObserved(); // 別忘了呼叫這個,否則應用程式可能會異常終止!
};
詳情: TaskScheduler.UnobservedTaskException。
6. 有用的小細節
方式比較示意
| 方式 | 例外已處理? | 在哪裡捕捉錯誤 | 遺失錯誤的風險 |
|---|---|---|---|
|
是 | 在呼叫端的程式碼 | 低 |
| Fire-and-forget 未加 try/catch | 否 | 無處 | 非常高 |
| Fire-and-forget 加上 try/catch | 是 | 在任務內部 | 低(如果有記錄) |
| async void 方法 | 否(會飛到全域) | 全域處理器 | 高 |
如何正確設計 fire-and-forget
- 如果任務的結果或狀態很重要——別用 fire-and-forget。使用 await 或保留 Task 以便之後等待。
- 只有在真正不重要的背景工作才使用 fire-and-forget(例如發送遙測)。
- 一定要把 fire-and-forget 包成自己的方法並捕捉/記錄例外。
- 對於複雜的背景場景,使用佇列和 worker:Hangfire, Quartz.NET。
實務應用與面試重點
面試常問:「如果 fire-and-forget 任務發生例外會怎樣?」或「為什麼不能到處用 async void?」正確的回答:你要自行負責背景任務的錯誤命運——要麼捕捉、記錄並分析,要麼你就會得到幽靈 bug。
比較 'fire-and-forget' 與 await 的適用性
| 情境 | 錯誤處理可靠性 | 適用性 |
|---|---|---|
| 一般 await | 很好 | 任何需要結果或重視成功/失敗的地方 |
| Fire-and-forget | 差(如果不手動處理) | 只適合真的背景且不重要的任務 |
| Fire-and-forget + try/catch | 好(如果有記錄) | 結果不需要但需要知道是否失敗的背景任務 |
下一講會討論在回傳多個結果的平行任務中如何處理錯誤。暫時記得:如果你開了一槍(fire),別忘了確認子彈有沒有命中目標!
7. 使用 fire-and-forget 任務時的常見錯誤
錯誤 #1:忽略 fire-and-forget 的例外。
新手以為例外會「哪裡冒出來」。沒有 try-catch 和日誌,它們會丟失,導致難以發現的 bug。
錯誤 #2:在非事件處理器中使用 async void。
這類方法會把例外丟到全域處理器(例如 AppDomain.UnhandledException),可能會使應用程式異常終止。
錯誤 #3:過度捕捉例外。
在任務內捕捉所有例外可能會隱藏真正應該在呼叫端處理的問題,增加除錯難度。
錯誤 #4:忽視日誌記錄。
沒有針對 fire-and-forget 任務的日誌,尤其是在 production 環境下,根本不知道有沒有故障。
GO TO FULL VERSION