.NET October 25, 2025 • 6 min read • Threading

Threading in .NET: the mental model

By Omarr • Published: October 25, 2025

The hard part of threading in .NET isn’t the APIs, it’s the mental model. Here’s the way I explain it to teammates when we’re working on APIs and background services.

1. Threads vs tasks

Threads are OS-level workers. Tasks are logical units of work. A single thread can execute many tasks over time as they await I/O.

2. Async/await frees threads during I/O

In backend systems, most latency is I/O: HTTP calls, queues, SQL. Using async/await lets threads do other work while waiting, which increases throughput without “more threads”.

3. Avoid blocking on async

I avoid patterns like .Result, .Wait(), and Task.Run in ASP.NET:

  • They can deadlock or starve the thread pool.
  • They make behavior under load unpredictable.

Once a pipeline is async, keep it async all the way down.

4. Concurrency vs parallelism

I treat them differently:

  • Concurrency: multiple things in flight at once (e.g., multiple HTTP requests).
  • Parallelism: splitting CPU-bound work across cores.

For CPU-bound jobs I’ll use Parallel.ForEachAsync or channels + worker tasks with a clear degree-of-parallelism.

5. Shared state is the real enemy

Threading bugs almost always come from shared state. To avoid them:

  • Prefer immutable data passed between tasks.
  • Keep shared mutable state behind a single owner (e.g., a worker loop).
  • Use locks sparingly and around tiny critical sections.

← Back to all articles