foreach 中直接 await 是顺序执行
在
foreach循环体内对每个
Task使用
await,会等前一个任务完成后再启动下一个,本质上是串行的。这不是语法限制,而是
await的语义决定的:它会暂停当前方法执行,直到被等待的
Task完成。
常见错误现象:误以为“写了异步代码就自动并行”,结果接口响应时间随元素数量线性增长,比如处理 100 个 ID,每个 HTTP 请求耗时 200ms,总耗时接近 20 秒。
每次await都会挂起当前
async方法,控制权交还给调用方 下一次循环迭代必须等上一次
await返回后才开始 即使每个任务本身是 I/O 异步(如
HttpClient.GetAsync),它们也不会重叠发起
想并行执行得先启动所有任务再 await
要真正并发执行多个异步操作,必须把所有
Task对象先构造出来(即“火起来”),再统一
await Task.WhenAll(...)。关键点在于:**启动和等待要分离**。
使用场景:批量获取远程数据、并行验证多个输入、同时写入多个文件等 I/O 密集型操作。
Task.WhenAll接收的是
Task[]或
IEnumerable<task></task>,不是
async方法调用本身 如果在
Select中直接写
async x => await DoAsync(x),会返回
Task<task></task>,必须用
.Unwrap()或改用
Select(x => DoAsync(x))所有任务几乎同时发起,但异常会全部抛出(
AggregateException),需注意错误处理方式
var tasks = items.Select(item => FetchDataAsync(item)).ToArray(); await Task.WhenAll(tasks); // 所有请求并发发出,等待全部完成
别混淆 Parallel.ForEach 和 async/await
Parallel.ForEach是同步并行(基于线程池),不能直接
await异步方法;强行在其中
await会导致死锁或降级为同步阻塞(如调用
.Result或
.Wait())。
典型错误写法:
Parallel.ForEach(items, async item => {
await DoAsync(item); // 编译警告 CS1998,实际不会真正 await
});
Parallel.ForEach的委托签名是
Action<t></t>,不支持
async void或
async Task编译器会忽略
async关键字,内部变成同步执行,或因上下文丢失引发异常 真正需要 CPU 密集型并行 + 异步混合时,应考虑
Task.Run包裹同步计算,再组合
Task.WhenAll
性能与资源控制的实际取舍
盲目并发所有任务可能压垮服务端(如触发限流)、耗尽连接池或导致本地线程饥饿。真实项目中往往需要节流。
推荐做法不是“全量并发”,而是可控并发:
用SemaphoreSlim限制最大并发数,例如只允许同时 5 个 HTTP 请求 避免在循环中反复创建
HttpClient实例,复用单例或
IHttpClientFactory注意
Task.WhenAll失败时的默认行为:只要一个失败,整个就失败;如需“尽力而为”,得用
Task.WhenAll(tasks).ContinueWith(...)或手动遍历
task.Exception
最易被忽略的一点:await 的位置决定了控制流形状——它不在循环体里,就在循环外;不在
WhenAll前,就在它后面。写错一行,顺序和并行就彻底反了。
