c# 在 foreach 循环中 await 任务是顺序执行还是并行执行

来源:这里教程网 时间:2026-02-21 17:40:06 作者:

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
前,就在它后面。写错一行,顺序和并行就彻底反了。

相关推荐