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.
- 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.
- 📜 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.
- 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");
});
👉 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");
👉 Here, Task.Run makes sure the CPU-heavy task doesn’t block your main thread (like an ASP.NET request or UI).
- Mistakes to avoid
❌ Wrapping async calls in Task.Run
// Bad: don’t wrap async I/O
await Task.Run(() => httpClient.GetStringAsync(url));
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
});
This eats threads without benefit. Use Task.Run or Parallel.For instead.
- 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”
- 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)