1. Task.WhenAll:同时等待所有任务
现实中很少只有一件事要做。你起床时看下表、洗个澡唱会儿歌、穿衣服听音乐,同时还在煮咖啡。或者在电脑上,你可能同时上传几个文件、发送一堆网络请求、按照不同方向计算大量数据。所有这些操作都可以(也应该)并行执行,这样就不会浪费用户的时间。我们不想一个接一个地等每个任务完成!怎么组织这种“管弦乐”?怎么判断什么时候全部都完成了?或者相反,怎么知道谁先完成?
Task.WhenAll 和 Task.WhenAny 就是为这些场景准备的工具。
WhenAll 方法
Task.WhenAll 是类 Task 的静态方法,它接收一个任务集合并返回一个新任务,这个新任务只有在所有传入任务都完成后才会结束。就像考试时你等着所有同学交完答卷才离开考场一样。
方法签名
Task Task.WhenAll(params Task[] tasks)
Task<TResult[]> Task.WhenAll<TResult>(params Task<TResult>[] tasks)
有返回结果的任务(Task<TResult>)和“空”的任务(Task)两种版本。
最简单的例子:等待所有文件下载完
我们来异步下载三个文件。为简单起见,用延迟(Task.Delay)来模拟下载。示例是真实可运行的代码。
using System;
using System.Threading.Tasks;
class Program
{
// 模拟延迟下载文件,返回文件名
static async Task<string> DownloadFileAsync(string fileName)
{
Console.WriteLine($"► 开始下载:{fileName}");
await Task.Delay(2000); // 等待 2 秒
Console.WriteLine($"✓ 下载完成:{fileName}");
return fileName;
}
static async Task Main()
{
var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
// 同时启动所有下载
var downloadTasks = new Task<string>[files.Length];
for (int i = 0; i < files.Length; i++)
{
downloadTasks[i] = DownloadFileAsync(files[i]);
}
// 等待所有下载完成
string[] results = await Task.WhenAll(downloadTasks);
Console.WriteLine($"所有下载完成!列表:{string.Join(", ", results)}");
}
}
这里发生了什么?
- 我们不是按顺序等待每个任务。所有三个同时启动(异步)。
- Task.WhenAll(downloadTasks) 返回一个任务,这个任务只有在所有子任务都完成后才会完成。
- 之后可以处理所有任务的结果 —— 使用它们、输出到屏幕、继续传递给别的逻辑。
类比
这就像你派了三个快递员去送三个包裹,只有当所有人都把包裹送到客户手中,你才向老板报告“都送完了”。
Task.WhenAll 的工作流程示意
sequenceDiagram
participant Program
participant 任务1
participant 任务2
participant 任务3
Program->>任务1: 启动
Program->>任务2: 启动
Program->>任务3: 启动
任务1-->>Program: 完成(可能比其他提前)
任务2-->>Program: 完成
任务3-->>Program: 完成
Program->>Program: Task.WhenAll 完成,所有结果可用
如果其中一个任务抛出异常会怎样?
Task.WhenAll 不会在某个任务出错时立即中断。它会等所有任务都结束。如果至少有一个任务发生异常,最终任务会处于 Faulted 状态,并且包含所有发生的异常的集合。
示例
static async Task<string> MayThrowAsync(string fileName)
{
await Task.Delay(500);
if (fileName == "fileB.txt")
throw new Exception("下载 fileB.txt 时出错");
return fileName;
}
static async Task Main()
{
var files = new[] { "fileA.txt", "fileB.txt", "fileC.txt" };
var downloadTasks = files.Select(MayThrowAsync).ToArray();
try
{
string[] results = await Task.WhenAll(downloadTasks);
Console.WriteLine($"一切顺利:{string.Join(", ", results)}");
}
catch (Exception ex)
{
Console.WriteLine($"至少有一个任务出错:{ex.Message}");
}
}
如果发生异常,想查看所有内部错误,请使用 AggregateException.InnerExceptions。
官方文档: Task.WhenAll docs
2. Task.WhenAny:等待第一个完成的任务
Task.WhenAny 也是静态方法,但它在任意一个传入任务进入“完成”状态(成功或失败)时就结束。它返回第一个完成的任务的引用。
签名
Task<Task> Task.WhenAny(params Task[] tasks)
Task<Task<TResult>> Task.WhenAny<TResult>(params Task<TResult>[] tasks)
类比
谁先把蛋糕烤好谁就厉害,有时候你只需要知道哪个服务器最快响应,然后用它的结果。
示例:谁更快?
using System;
using System.Threading.Tasks;
class Program
{
// 模拟不同速度的请求
static async Task<string> RequestAsync(string name, int delay)
{
await Task.Delay(delay);
return $"{name} 完成耗时 {delay} 毫秒";
}
static async Task Main()
{
var taskA = RequestAsync("A", 1000);
var taskB = RequestAsync("B", 700); // 最快
var taskC = RequestAsync("C", 1500);
// 等待第一个完成的任务
Task<string> finished = await Task.WhenAny(taskA, taskB, taskC);
Console.WriteLine($"最先完成的是:{finished.Result}");
}
}
用 WhenAny 我们可以知道哪个任务先到达终点。之后可以取消其余任务,例如用 CancellationToken(下一些讲会讲)。
为什么 WhenAny 返回的是任务而不是结果?
因为方法不知道哪个任务会先完成:它无法提前知道哪个任务的返回类型是什么。所以它返回完成的任务本身 —— 我们需要自己取出结果:用 await 或访问该任务的 Result。
WhenAll vs WhenAny:对照说明
| 方法 | 何时触发 | 返回什么 | 典型场景 |
|---|---|---|---|
|
当 全部 完成时 | 任务(Task/Task<T[]>) | 等所有响应或所有文件,聚合结果 |
|
当任意一个任务完成时 | 最先完成的那个任务 | 使用第一个结果,取消其余任务 |
3. 结合 WhenAll 和 WhenAny — 真实场景
有时需要先等任意一个任务完成,再等全部完成;或者反过来。
示例:两个服务器之间的“乒乓”
假设你同时向两个克隆服务器发起请求,以防其中一个变慢。但你会使用第一个返回的结果。
static async Task Main()
{
var fastServer = RequestAsync("快速服务器", 400); // 400 ms
var slowServer = RequestAsync("慢速服务器", 2000); // 2000 ms
var completed = await Task.WhenAny(fastServer, slowServer);
Console.WriteLine(await completed);
// *可选*:这里可以通过 CancellationToken 取消尚未完成的任务
}
示例:只有在所有数据都加载完后才开始处理
你的应用有三个数据源。只有当三个都加载完,才应该进行处理:
static async Task Main()
{
var task1 = DownloadFileAsync("a.txt");
var task2 = DownloadFileAsync("b.txt");
var task3 = DownloadFileAsync("c.txt");
var all = await Task.WhenAll(task1, task2, task3);
ProcessFiles(all[0], all[1], all[2]);
}
4. 使用 Task.WhenAll 和 Task.WhenAny 的常见错误
错误 №1:以为 WhenAll 在第一个任务完成时就结束。
新手常以为 Task.WhenAll 会在第一个任务完成时返回。其实它会等待所有任务完成。
错误 №2:忽视 WhenAll 中的异常。
如果某个任务抛出异常,最终任务会处于 Faulted 状态。不处理 AggregateException.InnerExceptions 可能会漏掉重要错误。
错误 №3:在 WhenAny 中不检查就直接访问 Result。
如果第一个完成的任务本身抛了异常,访问 Result 会抛出异常。访问之前请检查 Task.IsFaulted。
错误 №4:把任务串行化而不是并行化。
例如在循环里对每个任务都用 await,而不是用 Task.WhenAll,会大幅降低性能。
GO TO FULL VERSION