Chapter 06 Async Implementation
3 min read- Chapter 06 — Async implementation
- 6.1 Structure of the generated code
- 6.1.1 The stub method
- 6.1.2 Structure of the state machine
- 6.1.3 `MoveNext()` (high level)
- 6.1.4 `SetStateMachine` and the boxing dance
- 6.2 A simple `MoveNext()` implementation
- 6.2.3 Zooming into an `await`
- 6.3 How control flow affects `MoveNext()`
- 6.3.1 Control flow between awaits is simple
- 6.3.3 Awaiting inside `try/finally`
- 6.4 Execution contexts and flow
- 6.5 Custom task types revisited
Chapter 06 — Async implementation
Purpose of this chapter: understand what the compiler generates for async/await so you can reason about performance, debugging, and “weird” control-flow issues (especially around try/finally, contexts, and custom awaitables).
This is implementation detail knowledge: it can change between compiler versions, but the mental model is still useful.
6.1 Structure of the generated code
Async methods compile to a state machine.
At a high level, the compiler generates:
- A “stub” method with the original signature (it constructs/starts the state machine and returns a task).
- A state machine type (often a struct in release builds) with a
MoveNext()method that advances execution.
Terminology that helps:
- A method “takes a step” when
MoveNext()runs until the next suspension point. - It “pauses” when it reaches an
awaitwhose awaiter isn’t completed yet; it schedules a continuation and returns.
6.1.1 The stub method
What the stub effectively does:
- Create the state machine instance.
- Initialize a builder (task builder) + set initial state.
- Call
MoveNext()once to run until the first incomplete await. - Return the task from the builder.
6.1.2 Structure of the state machine
The state machine stores:
- A state integer (which “await point” you’re at).
- Any locals that must live across awaits.
- Awaiters for incomplete awaits.
- The builder used to produce/complete the returned task.
6.1.3 MoveNext() (high level)
MoveNext() is basically:
- A big switch on the current state.
- Execute code until:
- it completes successfully (set result), or
- it faults (set exception), or
- it hits an incomplete await (store state/awaiter + schedule continuation).
6.1.4 SetStateMachine and the boxing dance
Implementation detail that matters for perf intuition:
- State machines may be structs, but can be boxed (e.g., for interface calls/debugger scenarios).
- Debug vs release builds can differ (debug may use classes for better debugging support).
6.2 A simple MoveNext() implementation
Important mental model:
- Every
awaitturns into: - “Get awaiter”
- “If not complete, store state + schedule continuation”
- “On resume, retrieve awaiter and get result (or throw)”
6.2.3 Zooming into an await
Core mechanics:
GetAwaiter()produces an awaiter.- If not completed:
- set state (so resume knows where to continue)
- register continuation (so
MoveNext()is called again later) - return
- If completed:
- proceed synchronously without suspension
Interview nuance:
- This explains why “already completed tasks” can run synchronously (sometimes surprising).
6.3 How control flow affects MoveNext()
6.3.1 Control flow between awaits is simple
Between await points, the code is straightforward.
6.3.3 Awaiting inside try/finally
This is where it gets interesting:
- The compiler must preserve
finallysemantics even when control suspends. - The state machine needs to ensure cleanup runs on all completion paths (success/fault/cancel) and on early exit.
Interview angle:
- “Why is
awaitinsidefinallytricky?” → because the runtime must ensurefinallyexecutes correctly even though the method can pause and resume later.
6.4 Execution contexts and flow
Two concepts often conflated:
- Synchronization context / scheduler: where continuations run.
- Execution context: ambient data flowing across async boundaries (e.g.,
AsyncLocal<T>, security/culture/context data).
Performance intuition:
- Context capture/flow can add overhead; understand when you’re paying for it and why.
6.5 Custom task types revisited
Why the internals matter:
- Custom awaitables/task-like types must integrate with the generated state machine expectations (awaiter pattern + completion signaling).
Practical guidance:
- Most teams should stick to
Task/ValueTaskunless they have a measured, well-understood reason not to.