c# 如何在循环中正确使用 async await

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

为什么在 for 循环里直接 await 会变慢

因为

await
默认是顺序等待:前一个异步操作没完成,后一个根本不会发起。比如调用 10 次
HttpClient.GetAsync()
,实际是串行发请求,总耗时 ≈ 所有请求耗时之和。

这不是 await 本身的问题,而是写法让它“不敢并发”。关键在控制权交还时机和任务调度逻辑。

如何让多个 async 调用真正并发执行

把异步操作包装成

Task
,先全部启动,再统一
await Task.WhenAll(...)
等待全部完成。

✅ 正确做法:用
var tasks = urls.Select(url => client.GetAsync(url)).ToArray(); await Task.WhenAll(tasks);</li>
<li>❌ 错误写法:<code>for (int i = 0; i < urls.Length; i++) { await client.GetAsync(urls[i]); }
—— 完全串行
⚠️ 注意:
Task.WhenAll
不会改变异常行为 —— 任一任务失败,整个
await
就抛出
AggregateException
,需用
try/catch
或检查
task.Exception
⚠️ 内存与连接数:并发太多可能触发
HttpClient
连接池限制或服务器限流,建议配合
SemaphoreSlim
限流

需要按顺序处理结果时怎么写

Task.WhenAll
返回的
Task<t></t>
结果数组,下标和原始输入顺序严格一致。不需要额外排序或映射。

var tasks = ids.Select(async id => {
    var res = await client.GetAsync($"/api/item/{id}");
    return await res.Content.ReadFromJsonAsync<Item>();
});
var results = await Task.WhenAll(tasks); // results[0] 对应 ids[0] 的结果

如果中间某次请求失败,对应位置的

results[i]
会是
null
(除非你显式 throw),但更稳妥的是用
Task.WhenAll
+ 单独 try/catch 包裹每个 lambda。

foreach 里用 async lambda 为什么编译不过

因为

async void
async Func<t></t>
foreach
中容易捕获错误的变量(闭包陷阱),且编译器不支持直接在
foreach
语句块中写
await
(会报 CS1992 “无法在匿名方法、lambda 表达式或查询表达式中使用 await”)。

✅ 解决:改用
Select
+
async
lambda,或提前把循环变量复制到局部变量(如
var current = item;
✅ 更安全写法:
foreach (var item in list) { var t = ProcessAsync(item); tasks.Add(t); }
,然后
await Task.WhenAll(tasks)
⚠️ 切勿写:
foreach (var item in list) { await ProcessAsync(item); }
—— 又回到串行
真正难的不是语法,是判断哪些操作必须等、哪些可以放一起跑,以及失败后要不要重试、要不要记录中间状态。这些没法靠
await
自动解决。

相关推荐