Lessons in this course (11)

10. Threads, the thread pool and how async/await works inside

Intermediate★ Must knowLesson 10 of 11

Managed threads map 1:1 to OS threads. Creating them is expensive, so .NET uses the ThreadPool: worker threads that run queued work items (Task.Run, timers, continuations) plus I/O completion handling. It injects threads gradually (hill-climbing) when work is queued faster than it completes — blocking calls cause thread-pool starvation (slow responses, growing queues).

async/await under the hood:

csharp
public async Task<int> GetCountAsync()
{
    var json = await http.GetStringAsync(url);   // 1. starts I/O, returns an incomplete Task
    return Parse(json);                           // 3. continuation after I/O completes
}
  1. The compiler rewrites the method into a state machine struct (IAsyncStateMachine) with a MoveNext() method; locals become fields.
  2. At await, if the task isn't complete, the state machine registers a continuation and returns to the caller — the thread goes back to the pool (no thread waits for the network).
  3. When the I/O completes (signalled by the OS via IOCP/epoll), a pool thread runs MoveNext() again from the saved state.
  4. SynchronizationContext: UI apps and classic ASP.NET resume on the original context (UI thread/request context); ASP.NET Core has no SynchronizationContext, so continuations run on any pool thread. ConfigureAwait(false) in libraries avoids capturing a context.
  5. Blocking on async (.Result, .Wait()) holds a thread and, with a single-threaded context, can deadlock.

ExecutionContext flows ambient data (AsyncLocal, culture, activity/trace id) across awaits automatically.