Parallel ForEach未等待HttpClient完成,finally提前执行问题咨询
咱们先还原一下你的问题场景:你用Parallel.ForEach调用包含HttpClient.GetAsync的异步方法Do1时,finally块里的"Done"会在HTTP请求完成前就打印出来;但换成调用Do2就正常,"Done"会在所有输出之后才出现。而且你明确希望继续用Parallel.ForEach而不是Task.WhenAll。
为什么HttpClient的场景会不符合预期?
核心原因是**Parallel.ForEach是为同步代码设计的,它完全不理解异步任务**。
当你写Parallel.ForEach(numbers, async (number) => { await Do1(); })的时候,这个异步lambda其实是async void类型(因为Parallel.ForEach接受的是Action<T>委托,异步版本会隐式转为async void)。而async void方法有个关键特性:它在遇到第一个await时就会立即返回,不会等待后续异步操作完成。
所以流程变成了这样:
Parallel.ForEach启动每个lambda,遇到await Do1()就直接认为这个委托执行完毕了- 很快所有lambda都"执行完",外层的
Task.Run任务也随之结束 - Main方法里的
await Task.Run(...)完成,直接进入finally块打印"Done" - 此时
Do1里的HTTP请求还在后台异步执行,等请求完成后才会打印StatusCode
而调用Do2时正常的原因是:Do2是async Task方法,当你在Main里await Do2()时,Main会等待Do2的整个执行流程完成——包括循环里的每一个await Multiply(i),所以所有输出都完成后才会进入finally块。
用Parallel.ForEach实现预期效果的解决方案
既然你想保留Parallel.ForEach,可以用以下两种方式让它等待所有异步任务完成:
方案1:同步等待异步任务(简单直接,但会阻塞线程)
把异步lambda改成同步委托,在里面用.GetAwaiter().GetResult()等待Do1完成,这样Parallel.ForEach会确保每个任务执行完再处理下一个:
await Task.Run(() => { Parallel.ForEach(numbers, (number) => { Do1().GetAwaiter().GetResult(); }); });
方案2:收集所有异步任务后统一等待(保留异步优势)
用线程安全的集合收集每个Do1返回的Task,在Parallel.ForEach启动完所有任务后,统一等待全部完成:
await Task.Run(() => { var tasks = new ConcurrentBag<Task>(); Parallel.ForEach(numbers, (number) => { tasks.Add(Do1()); }); Task.WaitAll(tasks.ToArray()); });
这个方案里Parallel.ForEach负责高效启动所有异步请求,之后Task.WaitAll确保所有请求完成后,外层的Task.Run才会结束,这样finally块就会在所有StatusCode输出后再打印"Done"。
内容的提问来源于stack exchange,提问作者Shak Ham

