未等待异步方法的foreach与Parallel.ForEach的差异探究
三种异步/并行循环实现的差异分析
三种实现代码
实现1:Async方法内的foreach(不等待异步任务)
public class MainClass { public async Task DoSomethingAsync() { foreach(OtherClass iterator in _otherClasses) { iterator.AnAsyncTask().ConfigureAwait(false); } } }
实现2:Parallel.ForEach搭配Async迭代器(等待异步任务)
public class MainClass { public void DoSomething() { Parallel.ForEach(_list, async iterator => { await iterator.AnAsyncTask().ConfigureAwait(false); }); } }
实现3:Parallel.ForEach搭配同步迭代器(不等待异步任务)
public class MainClass { public void DoSomething() { Parallel.ForEach(_list, iterator => { iterator.AnAsyncTask().ConfigureAwait(false); }); } }
疑问解答
实现1和Parallel.ForEach的并行效果一致吗?
完全不一样。
- 实现1的
foreach是单线程串行触发异步任务:循环会挨个调用AnAsyncTask(),但因为没加await,每个任务一启动就立刻进入下一次循环。虽然异步任务本身可能由线程池调度并行执行,但循环触发任务的过程是串行的,不是真正意义上的并行启动。 - 而
Parallel.ForEach是多线程并行触发任务:它会从线程池拿多个线程,同时启动多个迭代,任务的启动是并行进行的,不是逐个来的。
普通foreach和Parallel.ForEach在该场景下的核心差异
执行线程模型
- 普通
foreach:在当前线程(比如UI线程的话,会占着UI线程直到循环跑完,不过这里ConfigureAwait(false)不影响后续上下文)挨个触发异步任务,触发过程是同步的,只是任务本身是异步执行。 Parallel.ForEach:会根据CPU核心数等情况,从线程池拉多个线程并行跑迭代,同时触发多个异步任务,触发过程是并行的。
- 普通
异步任务等待的影响
- 实现1里不
await:所有异步任务一触发,DoSomethingAsync就直接返回了,不会等任何任务完成,甚至可能任务还没开始执行,方法就结束了。如果要等所有任务完成,得把每个Task收集起来,最后用await Task.WhenAll(tasks)。 - 实现2用了
async迭代器并await:Parallel.ForEach不认识异步委托,会把每个async委托当成async void处理——也就是说,Parallel.ForEach会在所有迭代的委托都启动后直接返回,不会等异步任务完成。而且async void的任务如果抛异常,根本没法捕获,会直接炸到线程池里。 - 实现3不
await:和实现2类似,Parallel.ForEach并行触发异步任务,但同样不会等任务完成,只是迭代器是同步的,触发完任务就立刻结束当前迭代。
- 实现1里不
资源占用
- 普通
foreach:资源消耗低,因为是单线程串行触发,不会额外占用线程池线程(除了异步任务本身可能用的),适合I/O密集型场景。 Parallel.ForEach:会占用多个线程池线程,更适合CPU密集型任务;对于I/O密集的异步任务,这种方式反而浪费线程——因为线程会在等待异步任务时闲置着。
- 普通
任务完成追踪
- 普通
foreach可以通过收集Task对象,用Task.WhenAll追踪所有任务完成。 Parallel.ForEach搭配异步委托时,没法直接追踪任务完成,因为它不支持等待异步任务,强行用async委托会导致程序没法正确等待所有任务结束。
- 普通
内容的提问来源于stack exchange,提问作者Álvaro García
相关产品推荐
相关产品推荐

