VOOZH about

URL: https://dev.to/libintombaby/threading-vs-tasks-vs-parallelism-the-complete-net-concurrency-guide-7dj

⇱ Threading vs Tasks vs Parallelism — The Complete .NET Concurrency Guide - DEV Community


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, and Parallel.


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

  • Thread is the OS-level primitive — powerful but expensive and rarely needed directly
  • ThreadPool recycles threads — Task.Run wraps it
  • Task is the standard abstraction for concurrent work
  • async/await frees threads during I/O — it is not parallelism
  • Parallel.For / Parallel.ForEach distributes CPU work across cores
  • I/O-bound → async/await; CPU-bound single op → Task.Run; CPU-bound many items → Parallel
  • Never use Parallel for 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."