foreach中直接await是顺序执行,因await会暂停当前方法直至任务完成;要并行需先用Select启动所有Task,再用Task.WhenAll统一等待。

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,不是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 和 async/await
Parallel.ForEach 是同步并行(基于线程池),不能直接 await 异步方法;强行在其中 await 会导致死锁或降级为同步阻塞(如调用 .Result 或 .Wait())。
Shell本身是一个用C语言编写的程序,它是用户使用Linux的桥梁。Shell既是一种命令语言,又是一种程序设计语言。作为命令语言,它交互式地解释和执行用户输入的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。它虽然不是Linux系统核心的一部分,但它调用了系统核心的大部分功能来执行程序、建立文件并以并行的方式协调各个程序的运行。因此,对于用户来说,shell是最重要的实用程序,深入了解和熟练掌握shell的特性极其使用方法,是用好Linux系统
典型错误写法:
Parallel.ForEach(items, async item => {
await DoAsync(item); // 编译警告 CS1998,实际不会真正 await
});
-
Parallel.ForEach的委托签名是Action,不支持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 前,就在它后面。写错一行,顺序和并行就彻底反了。









