DEV Community

Spyros Ponaris
Spyros Ponaris

Posted on

🚀 Parallel.ForEachAsync vs Task.Run in C#: A Beginner’s Guide

When you start writing C# code that needs to do things at the same time, two methods often come up:

  • Task.Run
  • Parallel.ForEachAsync

At first, they both look like “magic ways to run things in parallel.”
But actually, they are built for different jobs. Let’s explore them step by step.

  1. First things first: what do they do?

🟢 Task.Run

Think of Task.Run as saying:

👉 “Run this one heavy piece of work on a background thread so my app doesn’t freeze.”

It’s great for CPU-bound work like:

  • resizing an image
  • encrypting a file

running a long calculation

🟢 Parallel.ForEachAsync

This one is more like:
👉 “I have a list of things to process. Please handle them in parallel, but don’t overload me—keep it under control.”

It’s built for I/O-bound work like:

  • calling multiple APIs
  • downloading files
  • querying a database

It also lets you set a limit (MaxDegreeOfParallelism) so you don’t spam the server or your own machine.
And yes, it supports CancellationToken out of the box.

  1. 📜 A quick history lesson

When Parallel.ForEach was first introduced in .NET 4 (2010), it was designed for CPU-bound, in-memory operations — things like processing arrays, crunching numbers, or running loops over data in memory.

Back then, calling web services inside Parallel.ForEach was discouraged because:

  • Each request would block a thread while waiting.
  • This caused thread-pool starvation and poor scalability.

That’s why the advice was: “Parallel.ForEach is only for CPU-bound in-memory work.”

🔹 Fast forward to .NET 6 (2021): the .NET team introduced Parallel.ForEachAsync.

Why?

To bring the power of parallel loops to async/await workloads.

To make it safe and efficient to run I/O-bound tasks (like HTTP requests, DB queries, or file I/O) in parallel.

To give developers built-in throttling with MaxDegreeOfParallelism, so you don’t need to hand-roll SemaphoreSlim loops.

So if you’ve heard that “Parallel is only for in-memory data” — that’s true for the old synchronous API, but with .NET 6+, Parallel.ForEachAsync is the recommended way to handle async I/O in parallel.

  1. Examples Example A: Calling APIs in parallel (I/O-bound)

Here’s how you fetch multiple web pages at the same time:

var urls = new[]
{
    "https://api.site.com/page/1",
    "https://api.site.com/page/2",
    "https://api.site.com/page/3"
};

await Parallel.ForEachAsync(
    urls,
    new ParallelOptions { MaxDegreeOfParallelism = 3, CancellationToken = cancellationToken },
    async (url, ct) =>
    {
        var response = await httpClient.GetStringAsync(url, ct);
        Console.WriteLine($"{url} => {response.Length} chars");
    });
Enter fullscreen mode Exit fullscreen mode

👉 At most 3 requests run at the same time.
👉 Each call respects cancellation.
👉 No threads are wasted while waiting.

Example B: Heavy image processing (CPU-bound)

Now let’s say you’re processing a huge image. This is CPU work, not I/O.

var result = await Task.Run(
    () => HeavyImageProcessing(inputImage),
    cancellationToken);

Console.WriteLine($"Processed {result.Count} pixels");

Enter fullscreen mode Exit fullscreen mode

👉 Here, Task.Run makes sure the CPU-heavy task doesn’t block your main thread (like an ASP.NET request or UI).

  1. Mistakes to avoid

❌ Wrapping async calls in Task.Run

// Bad: don’t wrap async I/O

await Task.Run(() => httpClient.GetStringAsync(url));
Enter fullscreen mode Exit fullscreen mode

This just wastes a thread.

❌ Using Parallel.ForEachAsync for CPU-heavy loops

// Bad: don’t do CPU work here

await Parallel.ForEachAsync(files, async (file, ct) =>
{
    var data = ProcessFile(file); // CPU-heavy
});
Enter fullscreen mode Exit fullscreen mode

This eats threads without benefit. Use Task.Run or Parallel.For instead.

  1. Rule of Thumb

Here’s the cheat sheet 📝:

  • Use Parallel.ForEachAsync → many async I/O tasks (API calls, DB queries, file downloads).
  • Use Task.Run → single CPU-heavy job (calculations, image processing).
  • Both → accept a CancellationToken, so you can stop them if needed.

Think of it like this:

🔹 Parallel.ForEachAsync = “lots of async things at once”
🔹 Task.Run = “this one heavy thing, off the main thread”
🔹 CancellationToken = “and I can pull the plug anytime”

  1. Takeaway

Don’t mix them up:

  • If you’re waiting on the network, use Parallel.ForEachAsync.
  • If you’re burning the CPU, use Task.Run.

Both make your code faster and more responsive when used in the right place.

👉 Next step: Try rewriting one of your loops. Ask yourself: is this I/O or CPU? That’s how you’ll know which tool to grab.

📚 References

Top comments (0)