What is the difference between Parallel.ForEach and awaiting a set of tasks?

C# Async & Concurrency Advanced 3–5 Yrs5+ Yrs Conceptual 1 min read

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.ForEach for async I/O wastes threads (each blocks waiting) and doesn't compose with await.
  • Task.WhenAll on hundreds of I/O calls can overwhelm the downstream — throttle with SemaphoreSlim or Parallel.ForEachAsync's MaxDegreeOfParallelism.
  • Parallel.ForEachAsync (.NET 6) is the right tool: async body, degree-of-parallelism control, cancellation.