前言
你是不是也遇到过这样的情况:
明明写了个 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 编辑过