Operating systems and concurrency
Async I/O
Why waiting on the network should not hold a thread, how event loops and async functions handle thousands of connections, and when goroutines or thread pools fit better.
Most server time is spent waiting: for a database, a disk, or a remote API. A thread that blocks while it waits holds memory and a slot in the scheduler, and it can't serve anything else. A server that gives one thread per connection runs out of threads long before it runs out of CPU. This was the "C10K problem" of handling 10,000 concurrent connections, and async I/O is the answer.
The idea: ask the operating system to tell you when a connection is ready, and let one thread serve many connections in turn. Waiting becomes cheap, and the thread stays busy with work that is ready.
The big idea
Three layers work together:
- The kernel watches many file descriptors at once. Mechanisms include
select,poll,epollon Linux, andkqueueon BSD and macOS. Newerio_uringon Linux submits and completes I/O through shared ring buffers. - An event loop in your process asks the kernel what is ready, runs the callback or task for each, and repeats.
- Async functions let you write the callbacks as straight-line code. Each
awaitmarks a point where the function can pause and let the loop run something else.
Concepts in this topic
| Step | Concept | What it answers |
|---|---|---|
| 1 | Blocking I/O and epoll | How does one thread wait on thousands of sockets? |
| 2 | Event loops | How does the loop choose what to run next? |
| 3 | Async and await state machines | What does await actually do? |
| 4 | Goroutines and green threads | How do runtimes schedule many lightweight threads? |
| 5 | Backpressure and bounded queues | What stops a fast producer from flooding a slow consumer? |
Choosing a model
Async I/O is for many connections that mostly wait. It is not a way to speed up CPU work. A loop that runs a heavy computation blocks every other connection, so CPU-bound work belongs on a thread pool or in another process.
- Event loop (Node.js, Python asyncio, Nginx). Best for many connections, short handlers, and one or two cores per process. Run several processes for more cores.
- Threads with a pool (Java servlets, many Python and Ruby servers). Simple to reason about, and the blocking code you already have still works. Each thread costs memory and scheduling.
- Green threads (Go goroutines, Erlang processes, Java virtual threads). Write blocking-style code, and let the runtime multiplex many lightweight tasks onto a few OS threads.
Common mistakes
- Calling a blocking function, such as a synchronous file read or a CPU-heavy JSON parse, inside an event loop. Every other connection stalls behind it.
- Starting thousands of tasks with no limit, which fills memory with work the downstream can't absorb.
- Assuming async makes a slow query faster. It only frees the thread while the query runs.
Key takeaways
- Async I/O lets one thread serve many connections by waiting on the kernel for readiness.
- An event loop runs whichever task is ready, and
awaitmarks where a task yields. - Keep blocking and CPU-heavy work off the loop.
- Bound the work in flight, so the system degrades gracefully under load.
Further learning
Next, see blocking I/O and epoll for the kernel side of how one thread waits on many sockets.
Related
Concurrency
Why shared state breaks when two threads touch it at once, and the tools that fix it, from locks and semaphores to atomics, message passing and database constraints.
Processes, threads and the kernel
What the OS gives every program, and how to see it on a live Linux box. Start here for the process model.
Networking: IP, TCP and UDP
Packets, addresses, ports, and the handshakes and congestion control that decide how fast your bytes arrive. Explains why a new connection is slow and how to check it on the wire.
Backpressure and autoscaling
A queue only stores work, it doesn't create capacity. Learn how to tell when the backlog is growing, how to scale workers on the right signal, and what to do when you can't add more.