Task 与 Task<T>
本教程共 100 篇 · 第 82 篇 · 更新于 2026-07-31 · 约 7 分钟阅读
82. Task 与 Task
本节目标:学完能用 Task 表示”正在进行的异步工作”,理解 Task.Run 与 Wait 的基本用法及死锁风险。
Task 是什么
Task 是 .NET 里表示”一项进行中工作”的对象。它像一个收据:你交给系统一件事,系统回你一张 Task,凭它可查进度、拿结果。
没有 Task 的年代,异步靠回调,逻辑被拆得七零八落。Task 把”将来完成的操作”统一成对象,方便传递、组合、等待。
NoteTask 位于
System.Threading.Tasks命名空间。用顶级语句模板时,隐式全局 using 已帮你引入,不必手写。
Task 与 Task 的区别
Task 表示不返回值的异步操作,比如”把日志写完”。Task<T> 表示会带回结果的异步操作,比如”下载后返回字符串”。
async Task SaveLogAsync()
{
await Task.Delay(500); // 模拟写盘
Console.WriteLine("日志已保存");
}
async Task<int> AddAsync(int x, int y)
{
await Task.Delay(500);
return x + y;
}
await SaveLogAsync();
int sum = await AddAsync(3, 4);
Console.WriteLine($"结果:{sum}");
Task<int> 里的 int 就是将来拿到的结果类型。要取值,就 await 它。
Task.Run:把活丢给线程池
Task.Run 把一个方法丢到线程池(thread pool)去跑,立刻返回 Task,主线程不阻塞。
Task heavy = Task.Run(() =>
{
long total = 0;
for (int i = 0; i < 10_000_000; i++)
total += i;
Console.WriteLine($"算完:{total}");
});
Console.WriteLine("主线程没被卡住");
await heavy;
线程池会复用一组线程,避免频繁创建销毁。把重活丢进去,UI 或主流程仍能响应。
TipTask.Run 适合把 CPU 密集的活挪到后台。IO 密集的活别用 Task.Run 包一层,直接用原生异步 API 更省资源。
Wait:硬等,但有坑
Task.Wait() 会阻塞当前线程,直到任务完成。它和 await 最大区别:await 不阻塞线程,Wait 会。
Task task = Task.Delay(1000);
task.Wait(); // 当前线程被卡住 1 秒
Console.WriteLine("等完了");
Warning在 UI 或旧版 ASP.NET 上下文里,混用 await 和 Wait 极易”死锁(deadlock)“:上层等任务完,任务等上层让出线程,互相等。能用 await 就别用 Wait。
取结果的几种方式
拿到 Task<T> 后,最稳妥是 await 取值。也可用 Result 属性,但它等同于 Wait,同样会阻塞且可能死锁。
Task<int> task = AddAsync(10, 20);
int value = await task; // 推荐:不阻塞线程
Console.WriteLine(value);
Note记住一条铁律:库代码里返回的 Task,调用方用 await 接;除非万不得已,别在异步方法里调 Wait 或 Result。
Task 的内部状态
Task 有个 Status 属性,可能处于 WaitingToRun、Running、RanToCompletion、Faulted、Canceled 等状态。平时不用管,出错调试时可看。
任务若抛异常,不会静默消失。await 它时异常会重新抛出;若只 Wait 不看,异常被包进 AggregateException,容易被忽略。
Task faulted = Task.Run(() => throw new InvalidOperationException("出错了"));
try
{
await faulted;
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
Tip用 await 等任务,抛出的就是原始异常类型,比拆 AggregateException 省心,也更好排查。
组合多个任务
Task 类提供 WhenAll 与 WhenAny,等待一组任务全部或任意一个完成,这在并发请求时很常用。
Task a = Task.Delay(1000);
Task b = Task.Delay(1500);
await Task.WhenAll(a, b); // 等两者都完成
Console.WriteLine("都好了");
NoteWhenAll 不阻塞线程,只是”挂起”当前方法直到全部完成,和 await 一个道理。
已经完成的任务:CompletedTask 与 FromResult
有时方法逻辑简单,其实不需要异步,却要实现异步接口。这时别硬 await Task.Delay,直接用现成对象:
Task Done() => Task.CompletedTask; // 立刻"已完成"的任务
Task<int> Answer() => Task.FromResult(42); // 立刻带回结果的任务
Task.FromResult 返回已完成的 Task
Tip不要为了”看起来异步”而把同步方法包一层 Task.Run。能同步算完就直接返回 Task.FromResult,开销更小。
WhenAny:谁先完成先处理
Task.WhenAny 等”任意一个”任务完成,返回那个先结束的任务。适合”取最快响应”或”边等边收尾”。
Task a = Task.Delay(1000);
Task b = Task.Delay(500);
Task first = await Task.WhenAny(a, b);
Console.WriteLine(first == b ? "b 先好" : "a 先好");
注意 WhenAny 返回的是 Task<Task>:外层任务完成,内层才是真正先结束的那个。
Note多个下载比赛,谁快用谁——这是 WhenAny 的典型用法。但要记得真正 await 内层任务以拿结果或抛异常。
任务状态流转
一个 Task 大致经历:创建 → 等待运行 → 运行中 → 完成(成功/出错/取消)。平时不需手动管,但调试卡住的任务时,看 Status 能定位它卡在哪个阶段。
Task t = Task.Delay(100);
Console.WriteLine(t.Status); // 可能 WaitingToRun / RanToCompletion
await t;
Console.WriteLine(t.Status); // RanToCompletion
Warning不要轮询
while (!t.IsCompleted)空转等任务——那等于忙等,白白烧 CPU。要等就用 await 或 WhenAll。
Task 与线程的关系
要纠正一个直觉:一个 Task 不等于一个线程。Task 是”工作”的抽象,多数情况下由线程池里的某个线程代为执行,线程跑完就还回去。
这也是 Task 比直接 new Thread() 轻量的原因:线程池复用线程,省去反复创建销毁的开销。
Note长时间阻塞线程池线程(比如用 Wait)会耗尽线程,导致别的 Task 排队等不到线程。这正是别用 Wait 的深层理由。
Task.Run 的局限
Task.Run 适合把”现成的一段 CPU 活”挪到后台。但它不神奇:
- 它不能让一段同步代码变快,只是换了个线程跑。
- IO 操作本就有异步 API(如
File.ReadAllTextAsync),别用 Task.Run 包同步版,那是”假异步”,仍占线程。 - 过度 Task.Run 会让线程池频繁调度,反而更慢。
// 不推荐:把同步 IO 包成"异步"
// Task.Run(() => File.ReadAllText("a.txt"));
// 推荐:直接用原生异步 API
// await File.ReadAllTextAsync("a.txt");
Tip口诀:CPU 活上 Task.Run,IO 活上原生 Async。套错一层,性能与不阻塞的优势都没了。
等待多个任务的最佳实践
要并发等多个任务,就先收集 Task,再用 WhenAll。别在循环里逐个 await,那是串行。
List<Task<int>> tasks = [];
for (int i = 0; i < 3; i++)
tasks.Add(ComputeAsync(i));
int[] results = await Task.WhenAll(tasks);
Console.WriteLine($"全部结果:{string.Join(",", results)}");
async Task<int> ComputeAsync(int n)
{
await Task.Delay(100);
return n * n;
}
WarningWhenAll 中任一任务抛异常,整体会抛
AggregateException。想逐个处理异常,得分别 await 各子任务。
速查:Task 常用成员
记几个高频成员,写异步时随手可用:
Task.Status:任务当前状态(运行/完成/出错等)。Task.IsCompleted:是否结束(成功、出错、取消都算)。Task.WhenAll(...):等所有任务完成。Task.WhenAny(...):等任意一个完成。Task.FromResult(v):造一个已完成的 Task。 Task.CompletedTask:造一个已完成的 Task。task.Wait():阻塞等(慎用,易死锁)。
Task t = SomeAsync();
if (t.IsCompleted)
Console.WriteLine("已经结束了");
Tip日常 90% 场景用 await + WhenAll 就够。Wait 和 Result 尽量不碰,除非写无法 async 的旧代码。
常见混淆:Task 不是 Thread
新手常把 Task 想成”一个线程”。其实线程是执行单元,Task 是”工作”的描述;多个 Task 可能共用一个线程池线程,一个长任务也可能跨多个线程。
用 Thread 要自己管生灭,成本高;用 Task 交给线程池,几乎零管理。现代代码默认选 Task。
Note只有极特殊需求(如设线程优先级、长期常驻后台线程)才碰
Thread。普通异步一律 Task。
为什么需要 Task:回到”没有它”的年代
早年的 .NET 还没有 Task。那时想异步,要么自己开 Thread,要么用”异步编程模型(APM,Asynchronous Programming Model)“:成对出现 BeginXxx 与 EndXxx,再用 IAsyncResult 收尾。代码被切成几段,逻辑跳转难读,异常也难接。
Task 的出现(.NET 4.0)把”将来会完成的一件事”统一成一个对象:它有状态、能拿结果、能串联、能被 await。你不必管底层是开线程还是用 IO 完成端口,只要和 Task 打交道即可。这是异步写法能从”回调地狱”走向”同步风格”的根基。
Note把 Task 想成”快递单号”:下单后你拿到单号(Task),随时能查到哪了、货到了没。至于快递公司怎么调度车辆,你不必操心。
Task 与 Thread、委托的对比
| 方式 | 用法直觉 | 缺点 |
|---|---|---|
Thread | 自己造一个执行流 | 创建销毁贵,难管理,易出错 |
委托 BeginInvoke | 旧式异步回调 | 回调割裂,异常模型割裂 |
Task | 一个”将来完成”的对象 | 几乎无——现代首选 |
日常写异步,默认选 Task。只有在需要长期常驻、或要设线程优先级等极端场景,才考虑直接 Thread。
Tip口诀:能 Task 就 Task,别碰裸
Thread。Task 交给线程池统一调度,省心又不易错。
ContinueWith 与 await 的对比
在 async/await 出现前,链式处理靠 Task.ContinueWith:
namespace CSharpDemo;
Task<int> task = Task.Run(() => 21);
task.ContinueWith(t => Console.WriteLine($"结果:{t.Result}"));
ContinueWith 把”下一步”塞进回调,多层嵌套又回到割裂感。而 await 写法保持顺序阅读:
namespace CSharpDemo;
int result = await Task.Run(() => 21);
Console.WriteLine($"结果:{result}");
两者效果一样,但 await 版好读、好调试、异常也按同步模型走。所以能用 await 就别用 ContinueWith。
Warning
ContinueWith默认不回到原上下文,且异常处理麻烦,新手极易踩坑。现代代码一律优先 await。
适用场景速判
- 发起网络请求、读文件、延时等待 → 返回
Task/Task<T>,用await接。 - 一段纯 CPU 重活想挪到后台 → 用
Task.Run包一层。 - 多个独立工作要同时进行 → 收集 Task 用
WhenAll。 - 只是”可能异步”的高频小方法 → 考虑
ValueTask(见第 84 章)。
Tip别为了异步而异步。方法里没有真正的等待点,就保持同步,返回普通值即可。
小结
Task 是异步工作的”收据”,Task