What is the difference between Parallel.ForEach and awaiting a set of tasks?
Quick Answer
Parallel.ForEach is for CPU-bound work — it spreads iterations across cores and blocks the calling thread until done. For I/O-bound work you want await Task.WhenAll(items.Select(DoAsync)) (or Parallel.ForEachAsync in .NET 6+) — no threads are blocked while waiting, and you can cap concurrency.
Parallel.ForEach is data parallelism for CPU work — it takes a collection and runs the loop body across all your cores, blocking until everything's done. If the work is I/O — calling an API for each item — that's the wrong tool, because each iteration just blocks a thread waiting. There you want Task.WhenAll over the async calls, or better, Parallel.ForEachAsync from .NET 6, which has an async body and lets you set the max degree of parallelism so you don't fire five hundred requests at a downstream service at once.
- Using
Parallel.ForEachfor async I/O wastes threads (each blocks waiting) and doesn't compose withawait. Task.WhenAllon hundreds of I/O calls can overwhelm the downstream — throttle withSemaphoreSlimorParallel.ForEachAsync'sMaxDegreeOfParallelism.Parallel.ForEachAsync(.NET 6) is the right tool: async body, degree-of-parallelism control, cancellation.