




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