.NET Academy

Tasks and threads

Understand what a Task represents, how it relates to threads, and how to run work concurrently.

11 min read

You will learn

  • Explain the difference between a thread and a task
  • Describe the thread pool
  • Start work with Task.Run and wait for delays with Task.Delay
  • Combine tasks with Task.WhenAll and Task.WhenAny

Waiting is the normal case

Most of what a backend does is waiting: for the database, for another HTTP service, for a file, for a message. While it waits, the CPU has nothing to do. Asynchronous programming is about not wasting resources during those waits — and about doing several things at once.

Threads

A thread is the operating system's unit of execution: it has its own stack and runs code on a CPU core. Threads are relatively expensive — each reserves about 1 MB of stack, and switching between thousands of them costs CPU time.

Rather than creating threads, .NET keeps a thread pool: a set of reusable worker threads that pick up queued work items. ASP.NET Core handles each incoming request on a pool thread.

Tasks

A Task represents an operation that will complete in the future; Task<T> also produces a result of type T. A task isn't a thread:

  • A CPU-bound task (Task.Run(() => Compute())) runs on a pool thread.
  • An I/O-bound task (httpClient.GetStringAsync(url), Task.Delay(1000)) usually uses no thread at all while waiting — the OS or a timer signals completion.
  • A task can already be completed (Task.FromResult(42)).
Task<string> download = httpClient.GetStringAsync("https://example.com");
// ... do other things while the download is in flight ...
string html = await download;     // get the result when you need it

The thread pool under load

Here's why this matters for servers. Compare a server whose code blocks while the database works with one that awaits:

Interactive

Threads: blocking vs async/await

Each request: 1 unit of CPU work, a 4-unit database call, then 1 more unit of CPU

01234567891011

Requests

Request 1T1T1done at 6
Request 2T2T2done at 6
Request 3T1T1done at 12
Request 4T2T2done at 12

Thread pool

Thread 11133
Thread 22244
T1CPU work on a threadWaiting for the databaseQueued: no free thread

All requests finished at

t = 12

async: 7

Average response time

9.0

async: 6.5

Thread time wasted blocked

16 units

async: 0

With blocking code (var rows = db.Query(...) or .Result), a thread sits idle while the database works — the striped cells. Other requests queue up even though the CPU has nothing to do. Switch to async / await to compare.

With blocking code, threads sit idle holding requests hostage. With await, a few threads serve many concurrent requests. That's the core reason ASP.NET Core APIs are async all the way down.

Running work concurrently

Start several operations, then wait for all of them:

Task<User> userTask = GetUserAsync(id);
Task<List<Order>> ordersTask = GetOrdersAsync(id);
Task<decimal> balanceTask = GetBalanceAsync(id);
 
await Task.WhenAll(userTask, ordersTask, balanceTask);   // all three in flight at once
 
var user = userTask.Result;      // safe here: the task has already completed
MethodCompletes when
Task.WhenAll(tasks)All tasks complete (and gives all results for Task<T>)
Task.WhenAny(tasks)Any task completes — e.g. "first response wins" or timeouts
Task.Delay(ms)After the delay, without blocking a thread
Task.Run(work)The CPU work queued to the thread pool has finished

Don't block on tasks

task.Result and task.Wait() block the current thread until the task finishes — wasting it, and in some environments causing deadlocks. Use await instead. (Reading .Result after await Task.WhenAll is fine, as above, because the tasks are already done.)

Try it

Program.cs
Loading editor…

C# runtime not loaded

Key takeaways

  • Threads are expensive OS resources; the thread pool reuses a small number of them.
  • A Task is a future result — often with no thread involved while it waits.
  • Task.WhenAll runs independent operations concurrently; Task.WhenAny takes the first to finish.
  • Use Task.Run for CPU-bound work only, and never block on tasks with .Result or .Wait().

Quiz

4 questions · 3 correct to pass
  1. 1.What does a Task<int> represent?

  2. 2.Does `await Task.Delay(1000)` keep a thread busy for a second?

  3. 3.Three independent calls each take 1 second. How long does `await Task.WhenAll(a, b, c)` take, if they were all started first?

  4. 4.When is Task.Run appropriate?

To complete this lesson

  • ○ Pass the quiz (3/4 correct)