上周我审批了一个拉取请求,里面同时扇出六个 HTTP 调用,再用那种从 2015 年就开始写的循环把结果一个个收回来:把所有异步操作丢进 List,一个 while 配上 WhenAny,Remove 掉先完成的那个、处理、继续。我眼皮都没眨就点了通过。
一个小时后才猛然意识到,runtime 从 .NET 9 起就内置了 WhenEach,它存在的意义正是把这个循环彻底删掉。然而,代码作者和我,一个都没切过去。
![]()
于是我做了自己最近对旧习惯常干的事:给它们掐个表。
你大概率今天还在写这个循环
它长这样:
var pending = new List>(tasks);while (pending.Count > 0)var done = await Task.WhenAny(pending);pending.Remove(done);sum += await done;}
这个循环之所以存在,是因为 WhenAll 只有一条死规矩:所有任务不全部完成,一个结果都别想用。假设一个仪表板界面要并排取五个服务的数据,那趟 150 毫秒的用户画像调用,必须等到那趟 1200 毫秒的遗留定价接口返回,才算数。挨个收结果的循环恰好解决了这个问题:谁先回来先给谁。而 WhenEach 干的就是同一件事,并且只需要三行。
我们把五个模拟服务调用抽成一个方法:
static Task[] FanOut() =>Call("profile", 150),Call("orders", 300),Call("recs", 500),Call("inventory", 800),Call("legacy-pricing", 1_200),];
先用 WhenAll 跑一遍:
var all = await Task.WhenAll(FanOut()); // 所有人等着
打印结果是,五个结果在 1204 毫秒处一次性可用。换成 WhenEach:
await foreach (var t in Task.WhenEach(FanOut())) // 谁先完先给谁Console.WriteLine($"{await t} usable at {sw.ElapsedMilliseconds} ms");
打印出来就是:用户画像在 150 毫秒可用,订单在 298 毫秒,推荐在 499 毫秒,库存在 799 毫秒,遗留定价在 1198 毫秒。这些延时是模拟的 Delay 调用,时间当然长得严丝合缝,要看的其实是“形状”,不是具体毫秒数。WhenEach 返回的是 IAsyncEnumerable,每个任务刚完成的那一瞬间就被 yield 出来——跟我们手写的 drain loop 一模一样的形状。正因如此,所有人都在手写那个循环。
旧循环到底有多贵
这部分我从来就没量过。每一轮迭代,都要拿“还活着的列表”调用一次 WhenAny。WhenAny 会把那个列表复制到一个新数组里,再给每个还没收摊的任务挂上一个回调。下一轮迭代又把 n-1 个任务的列表重新拷贝、重新挂回调,再下一轮是 n-2。假如扇出 5000 个任务,WhenAny 这条路径大约会注册 1250 万个回调。别忘了,List.Remove 本身还是线性扫描。这个循环悄悄呈现平方级开销。
我准备了完全相同的 5000 个任务(随机 1 到 50 毫秒延时,固定随机种子以保证每种策略干的是同一份活),用三种方式收割。环境是 .NET 10、Release 编译、一台小规格的 Linux 云容器。
下边是一次比较典型的实测结果:
WhenAll(基准):耗时 57 ms,CPU 时间 22 ms,分配 1.4 MB
WhenEach:耗时 50 ms,CPU 时间 18 ms,分配 1.3 MB
WhenAny drain 循环:耗时 470 ms,CPU 时间 310 ms,分配 110 MB
看到 110 MB 的那个数字时,我差点把咖啡喷到屏幕上。同样的任务,同样的完成顺序,仅仅因为那个看似无害的 while 循环,内存开销暴涨了八十多倍。CPU 时间也慢了十几倍。
而新方法用起来不过就是把 `Task.WhenAll` 换成 `Task.WhenEach`,再套一个 `await foreach`,就这么简单。十年养成的习惯,三行代码就能扔掉,顺带把内存和 CPU 一起省下来。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.