Understanding .NET async/await Through a Single Diagram
This article uses a single illustrative diagram to explain how .NET's async/await works, detailing thread release during I/O, overlapped I/O via IOCP on Windows, continuation scheduling, and differences on Linux with epoll, highlighting scalability and thread reuse benefits.
I strongly agree with the idea that a picture is worth a thousand words, so I like to use diagrams to explain technical concepts; this article uses a diagram to illustrate how .NET async/await works.
The main idea behind async/await is that when we wait on an ongoing I/O operation, the calling thread can be released to do other work, providing excellent thread reuse and therefore better scalability; fewer threads can handle the same number of operations compared with synchronous waiting.
In Windows this relies on overlapped I/O, which delegates the I/O operation to the operating system and only notifies the caller via a callback when the operation completes. The core component is the I/O Completion Port (IOCP).
IOCP is a highly efficient mechanism for asynchronous I/O in Windows; a single port can monitor a large number of concurrent operations. In .NET there is essentially one IOCP, observed by several IOCP threads managed by the ThreadPool. The typical number of IOCP threads equals the number of logical processors, though the ThreadPool can create up to 1,000 threads by default.
The callback for an I/O operation runs on one of the IOCP threads, sets the operation result, and decides whether to:
Execute the continuation directly on the IOCP thread, which avoids a context switch and cache thrashing but may pollute the IOCP thread with user code.
Schedule the continuation elsewhere, usually via a SynchronizationContext or TaskScheduler, so it runs on a regular work thread.
In most cases continuations are scheduled to work threads; a notable exception is the HttpClient continuation on Windows since .NET Core 2.1.
On Linux the epoll mechanism replaces IOCP. Epoll threads listen for events and schedule continuations to work threads (no inlining). Linux also uses AIO and io_uring to rebuild the ThreadPool behavior.
All of these points are linked together by a state‑machine instance that tracks state, executes continuations, and preserves context.
This picture reminds me of Stephen Cleary’s famous article “There Is No Thread (1)”, which explains that when an asynchronous I/O operation runs, no thread is consumed executing user code, even though an IOCP thread may be blocked to handle completion notifications.
Reference: https://blog.stephencleary.com/2013/11/there-is-no-thread.html
I also shared a Twitter image about the relationship between ThreadPool and IOCP, which sparked discussion; interested readers can view it at https://twitter.com/konradkokosa/status/1264855190131400704 .
English original article: https://tooslowexception.com/net-asyncawait-in-a-single-picture/
High Availability Architecture
Official account for High Availability Architecture.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.