首页 / C# 入门教程 / Task 与 Task<T>

C# 入门教程

Task 与 Task<T>

本教程共 100 篇 · 第 82 篇 · 更新于 2026-07-31 · 约 7 分钟阅读

C#C# 入门教程编程语言异步编程Task

82. Task 与 Task

本节目标:学完能用 Task 表示”正在进行的异步工作”,理解 Task.Run 与 Wait 的基本用法及死锁风险。

Task 是什么

Task 是 .NET 里表示”一项进行中工作”的对象。它像一个收据:你交给系统一件事,系统回你一张 Task,凭它可查进度、拿结果。

没有 Task 的年代,异步靠回调,逻辑被拆得七零八落。Task 把”将来完成的操作”统一成对象,方便传递、组合、等待。

Note

Task 位于 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 或主流程仍能响应。

Tip

Task.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 类提供 WhenAllWhenAny,等待一组任务全部或任意一个完成,这在并发请求时很常用。

Task a = Task.Delay(1000);
Task b = Task.Delay(1500);
await Task.WhenAll(a, b); // 等两者都完成
Console.WriteLine("都好了");
Note

WhenAll 不阻塞线程,只是”挂起”当前方法直到全部完成,和 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;
}
Warning

WhenAll 中任一任务抛异常,整体会抛 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)“:成对出现 BeginXxxEndXxx,再用 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 多带一个结果。Task.Run 把 CPU 活甩给线程池,Wait 会阻塞线程且易死锁——优先用 await。下一章正式拆解 async/await 语法。