Roadmap › .NET Internals — CLR, CTS, CLS, IL, JIT, Memory Management & Garbage Collection › Module: Execution Internals
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
}- The compiler rewrites the method into a state machine struct (
IAsyncStateMachine) with aMoveNext()method; locals become fields. - 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). - When the I/O completes (signalled by the OS via IOCP/epoll), a pool thread runs
MoveNext()again from the saved state. - 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. - 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.