Thread, ThreadPool, Task, Parallel.For, PLINQ, when to use each
Concurrency is one of the most misunderstood areas of .NET development.
Thread, Task, Parallel, async/await — developers often use these interchangeably without understanding what they actually do. The wrong choice costs you performance, correctness, and scalability.
This guide breaks down each concept clearly, with real-world guidance on when to use which.
⚠️ Note: Another post Async/Await in C# — A Deep Dive Into How Asynchronous Programming Really Works covers the difference between asynchrony and parallelism briefly. This post goes much deeper into the underlying primitives —
Thread,ThreadPool,Task, andParallel.
The Mental Model
Before the code, get the mental model right.
Concurrency — dealing with multiple things at once (managing)
Parallelism — doing multiple things at once (executing)
Asynchrony — starting something and doing other work while waiting
These are not the same thing.
Thread — The Low-Level Primitive
A Thread is an OS-level execution unit.
var thread = new Thread(() =>
{
Console.WriteLine($"Running on thread {Thread.CurrentThread.ManagedThreadId}");
});
thread.Start();
thread.Join(); // Wait for it to finish
What you get
- Full control over the thread lifecycle
- Can set priority, name, apartment state
- Can be a foreground or background thread
What it costs
- Creating a thread allocates ~1MB of stack memory
- OS scheduling overhead on every context switch
- You are responsible for lifecycle management
- Does not integrate with async/await
When to use Thread directly
Almost never in modern .NET. Use Task instead.
The only remaining valid use case is COM interop requiring a specific apartment state (STA thread for WinForms/WPF dialogs).
ThreadPool — Reusing Threads
The ThreadPool manages a pool of pre-created threads that can be reused.
ThreadPool.QueueUserWorkItem(_ =>
{
Console.WriteLine("Running on a pool thread");
});
You don't create threads — you submit work items and the pool decides which thread handles it.
Why this matters
- No thread creation overhead
- Threads are reused across work items
- The runtime tunes the pool size automatically
When to use ThreadPool directly
You don't. Task.Run() wraps ThreadPool and gives you a much better API.
Task — The Modern Standard
Task is the recommended abstraction for concurrent and asynchronous work in .NET.
// CPU-bound work on the ThreadPool
var task = Task.Run(() =>
{
return ExpensiveCalculation();
});
var result = await task;
Task represents a promise
A Task is a promise that work will complete at some point.
-
Task— work with no return value -
Task<T>— work that returns a value -
ValueTask<T>— lightweight Task for high-frequency paths
Combining Tasks
// Run two tasks in parallel and wait for both
var t1 = Task.Run(() => DoWork1());
var t2 = Task.Run(() => DoWork2());
await Task.WhenAll(t1, t2);
// Wait for the first one to finish
var winner = await Task.WhenAny(t1, t2);
async/await — Asynchrony Without Blocking
async/await is not about parallelism.
It is about freeing threads while waiting for I/O — network calls, database queries, file reads.
// The thread is released while waiting for the HTTP response
var response = await httpClient.GetAsync(url);
The critical distinction
| Scenario | Use |
|---|---|
| Waiting for a database query |
async/await
|
| Waiting for an HTTP call |
async/await
|
| Running a heavy calculation | Task.Run() |
| Running multiple calculations at once |
Parallel.For / Task.WhenAll
|
Parallel — True CPU Parallelism
Parallel splits work across multiple CPU cores simultaneously.
Parallel.For
Parallel.For(0, 1000, i =>
{
ProcessItem(i);
});
Parallel.ForEach
var items = GetLargeCollection();
Parallel.ForEach(items, item =>
{
Process(item);
});
PLINQ
var results = items
.AsParallel()
.Where(x => x.IsValid)
.Select(x => Transform(x))
.ToList();
When to use Parallel
Only for CPU-bound work that can be split into independent chunks.
Never use Parallel for I/O-bound work — it wastes threads and can cause thread starvation.
The Decision Framework
Is the work CPU-bound or I/O-bound?
│
├── I/O-bound (network, DB, file)
│ └── Use async/await
│ └── Task.Run is NOT needed here
│
└── CPU-bound (calculations, image processing)
│
├── Single heavy operation
│ └── Task.Run(() => HeavyWork())
│
└── Many independent items
└── Parallel.For / Parallel.ForEach / PLINQ
Common Mistakes
Mistake 1: Using Task.Run for I/O-bound work
// ❌ Wrong — wastes a thread waiting for I/O
var result = await Task.Run(() => httpClient.GetAsync(url));
// ✅ Correct — no thread needed while waiting
var result = await httpClient.GetAsync(url);
Mistake 2: Using async/await for CPU-bound work
// ❌ Looks async but blocks the thread
public async Task<int> ComputeAsync()
{
return HeavyCpuWork(); // No await — just pretending to be async
}
// ✅ Correct — offload to ThreadPool
public async Task<int> ComputeAsync()
{
return await Task.Run(() => HeavyCpuWork());
}
Mistake 3: Parallel for I/O
// ❌ This spawns threads and blocks them all waiting for HTTP
Parallel.ForEach(urls, url =>
{
var result = httpClient.GetAsync(url).Result; // Blocking!
});
// ✅ Use Task.WhenAll for parallel async I/O
var tasks = urls.Select(url => httpClient.GetAsync(url));
var results = await Task.WhenAll(tasks);
Interview-Ready Summary
-
Threadis the OS-level primitive — powerful but expensive and rarely needed directly -
ThreadPoolrecycles threads —Task.Runwraps it -
Taskis the standard abstraction for concurrent work -
async/awaitfrees threads during I/O — it is not parallelism -
Parallel.For/Parallel.ForEachdistributes CPU work across cores - I/O-bound →
async/await; CPU-bound single op →Task.Run; CPU-bound many items →Parallel - Never use
Parallelfor I/O — it wastes threads
A strong interview answer:
"Threads are the OS-level unit of execution — expensive to create. Tasks abstract over the ThreadPool and support async/await. Async/await is about freeing threads during I/O, not parallelism. Parallelism means actually executing work on multiple cores simultaneously — that's what Parallel.For and Task.WhenAll are for. The golden rule: async/await for I/O-bound, Parallel or Task.Run for CPU-bound."
Top comments (0)