C#中Task.WhenAll和Task.WhenAny的使用与区别小结
作者:无风听海
一、先给终极结论
Task.WhenAll 和 Task.WhenAny 都不是执行器,而是“完成信号的组合器”。
它们:
- ❌ 不创建线程
- ❌ 不调度任务
- ❌ 不产生并发
- ✅ 只监听 Task 的完成
并发发生在 Task 被“创建/启动”的那一刻,而不是 WhenAll/WhenAny。
二、两者的本质模型(抽象层)
1、 Task.WhenAll —— “全部完成门闩(AND Gate)”
T1 ─┐ T2 ─┼─▶ [ All Completed ] ─▶ Completed Task T3 ─┘
语义:
- 所有 Task 完成 → WhenAll 完成
- 任一 Task 失败 → WhenAll 失败(但仍等所有结束)
2、 Task.WhenAny —— “第一个完成门闩(OR Gate)”
T1 ─┐ T2 ─┼─▶ [ First Completed ] ─▶ Completed Task<Task> T3 ─┘
语义:
- 第一个完成者胜出
- 其他 Task 不受影响,继续运行
三、底层实现原理(核心机制)
1、WhenAll 的内部机制(简化版)
对每个 Task 注册 continuation
使用一个 原子计数器(remaining)
每完成一个 Task:
- 记录状态(成功 / 失败 / 取消)
Interlocked.Decrement(remaining)
当
remaining == 0:- 设置 WhenAll Task 的最终状态
📌 没有线程等待,完全事件驱动
2、WhenAny 的内部机制(简化版)
对每个 Task 注册 continuation
第一个完成的 Task:
- 调用
TrySetResult(task)
后续完成者:
- 直接忽略
📌 只有一个能“赢”,没有计数器
3、为什么 WhenAny 返回Task<Task>?
- 外层 Task:表示“谁先完成”
- 内层 Task:表示“完成的那个任务本身”
WhenAny 返回的是“胜者句柄”,不是结果
四、执行与调度层面的关键差异
| 维度 | WhenAll | WhenAny |
|---|---|---|
| 等待策略 | 全部 | 第一个 |
| 返回类型 | Task / Task<T[]> | Task |
| 是否阻塞线程 | ❌ | ❌ |
| continuation 数量 | N | N |
| 内部同步 | 原子计数 | CAS / TrySet |
| 并发控制 | ❌ | ❌ |
二者都只做“信号组合”,不做“任务调度”
五、异常语义(非常重要)
1、 WhenAll 的异常规则
所有 Task 都会执行到结束
如果有异常:
内部保存
AggregateExceptionawait WhenAll时:- 抛出 第一个异常
- 其他异常仍可从各 Task 中取
try
{
await Task.WhenAll(tasks);
}
catch
{
var all = tasks
.Where(t => t.IsFaulted)
.SelectMany(t => t.Exception!.InnerExceptions);
}
2、 WhenAny 的异常规则
如果“第一个完成的 Task”失败:
await completedTask会直接抛异常
其他 Task 的异常:
- 不会被观察
- 必须手动处理,否则可能变成未观察异常
📌 WhenAny + 异常 = 高风险组合
六、取消语义(常被误解)
1、WhenAll
如果任一 Task 被取消:
- WhenAll 最终可能是 Canceled
但:
- 不会主动取消其他 Task
2、 WhenAny
- 不会取消任何 Task
- 取消必须由你显式触发
var winner = await Task.WhenAny(tasks); cts.Cancel(); // 你自己的责任
七、性能特征(底层视角)
1、 WhenAll / WhenAny 本身的成本
- 少量对象分配
- N 个 continuation
- 原子操作 / CAS
👉 几乎可以忽略
2、真正的性能瓶颈来自:
- Task 创建方式
- IO / CPU 本身
- Task.Run / ThreadPool 使用
- 并发规模失控
八、典型使用范式(工程级)
1、 WhenAll —— 并发聚合(最常用)
var t1 = GetUserAsync();
var t2 = GetOrdersAsync();
await Task.WhenAll(t1, t2);
return new
{
User = await t1,
Orders = await t2
};
适用场景
- 多个独立 IO
- 聚合响应
- Web API
2、 WhenAny —— 竞速 / 超时 / 降级
超时模式
var work = DoWorkAsync();
var timeout = Task.Delay(2000);
if (await Task.WhenAny(work, timeout) == timeout)
throw new TimeoutException();
await work;
主备切换
var tasks = new[]
{
CallPrimaryAsync(),
CallSecondaryAsync()
};
var winner = await Task.WhenAny(tasks);
return await winner;
⚠️ 记得取消失败者
九、常见错误总结
❌ 把 WhenAll 当“并行器”
❌ 在循环里直接 await(伪并发)
❌ WhenAny 后忽略未完成 Task
❌ WhenAll + Task.Run(Web)
❌ 忽略异常和取消
十、设计层面的黄金准则
并发 = Task 创建时机
组合 = WhenAll / WhenAny
调度 = ThreadPool / Parallel
WhenAll / WhenAny 决定“怎么等”,
而不是“怎么跑”。
十一、一句话终极总结
Task.WhenAll 是一个原子计数器驱动的完成门闩
Task.WhenAny 是一个 CAS 驱动的竞速门闩
它们本身几乎“没有重量”,
但决定了整个 async 架构的形状。
到此这篇关于C#中Task.WhenAll和Task.WhenAny的使用与区别小结的文章就介绍到这了,更多相关C# Task.WhenAll和Task.WhenAny内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
