前言
你是不是也遇到過這樣的情況:
明明寫了個 Task.Run
看起來沒問題,結果運行的時候卻出奇奇怪怪的問題?
比如循環變量不對勁、程序卡死了、異常還悄無聲息地消失了……這哪是寫代碼啊,簡直像踩地雷。
其實,這些問題的背后,往往都藏著幾個常見的 Task
陷阱。
今天我們就來聊聊其中最經典的“三宗罪”——閉包陷阱、Result 死鎖陷阱、異常被吃陷阱。
準備好避開它們了嗎?Let’s go!
1. 閉包陷阱
這是新手最容易踩的第一個坑,尤其是在循環中使用 Task.Run
或 lambda 表達式
時。
比如下面這個例子:
for (int i = 0; i < 5; i++)
{
// 錯誤!所有任務都會看到i=5
Task.Run(() => Console.WriteLine(i));
}
這段代碼中的 lambda 表達式捕獲的是變量 i
的引用,而不是值。當所有任務真正開始執行時,循環早就結束了,此時 i
的值已經是 5
正確的做法應該是:
for (int i = 0; i < 5; i++)
{
int temp = i;
Task.Run(() => Console.WriteLine(temp));
}
記住:
在循環中使用 Task.Run
或 lambda
時,記得把循環變量賦值給一個臨時變量再使用,避免閉包帶來的副作用
2. Result 死鎖陷阱
這個陷阱特別喜歡出現在 UI 應用(比如 WPF、WinForms)或 ASP.NET 這類有同步上下文的環境中。
比如下面這個例子:
// 錯誤!在UI線程調用會死鎖
var result = GetDataAsync().Result;
async Task<string> GetDataAsync()
{
await Task.Delay(1000);
return "Data";
}
為什么會死鎖?因為:
GetDataAsync()
內部用了 await
,它會在當前同步上下文中繼續執行后續代碼。- 但主線程又在等
.Result
,導致互相等待,直接卡死!
正確的做法應該是:
var result = await GetDataAsync();
記住:
不要在 UI 或 ASP.NET 等同步上下文中使用 .Result
或 .Wait()
,推薦使用 await
替代。
3. 異常被吃陷阱
你以為在 Task
中拋出了異常就會看到錯誤信息?錯!如果不用正確的方式處理,Task
中的異常可能會悄無聲息地消失……
比如下面這個例子:
// 錯誤!異常不會自動拋出,也不會顯示在控制臺
// 因為 Task.Run 啟動的任務是異步執行的,
// 如果你不 await 它,也不調用 .Exception,那異常就像石沉大海一樣,根本沒人知道發生了什么!
Task.Run(() => { throw new Exception("Oops!"); });
正確的做法應該是:
try
{
await Task.Run(() => { throw new Exception("Oops!"); });
}
catch (Exception ex)
{
Console.WriteLine($"捕獲異常: {ex.Message}");
}
或者這樣:
Task task = Task.Run(() => { throw new Exception("Oops!"); });
task.ContinueWith(t =>
{
if (t.Exception != null)
{
Console.WriteLine($"任務失敗:" + t.Exception.InnerException.Message);
}
});
記住:
只要是異步任務,一定要用 await
或者檢查 Exception
屬性,否則異常會被“吞掉”
總結
Task 很強,但得小心用,
這些看似不起眼的小細節,如果不注意,輕則邏輯錯誤,重則程序崩潰甚至死鎖,后果不堪設想。
該文章在 2025/6/18 10:00:43 編輯過