What does it mean for a network socket read to be non-blocking, and how is that different from asynchronous (completion-based) I/O where the operating system performs the whole read and then tells you it finished?
answer
- would-block vs parked thread
- readiness = doorbell, completion = courier
- who owns the buffer
- copy on my thread vs kernel's
- regular files always report ready
basics
~20 sNon-blocking: the read returns instantly with whatever bytes already exist or a "not ready" answer, so the thread never sleeps but you must retry when told the handle is ready. Asynchronous: you hand the OS a buffer, it does the read, then signals completion.
solid answer
~50 sA **blocking** read parks the calling thread until bytes arrive. A **non-blocking** read never parks: it returns the bytes already buffered, or a distinct "would block, try again" answer. Because retrying in a spin loop wastes a core, non-blocking handles are always paired with a *readiness* notification call — "of these 10,000 handles, these three have data" — plus a dispatch loop. The copy still happens on my thread, at a moment I choose. **Asynchronous / completion-based** I/O is a different contract: I submit the operation *together with a buffer*, and later I am told "this operation finished, 4,096 bytes, no error". The kernel owns the buffer meanwhile and performs the transfer itself. So non-blocking answers *"can I act now?"*; asynchronous answers *"it is already done."* Both avoid parked threads; they differ in who performs the transfer and who owns the buffer.
code
text · 6 linesloop:
ready = wait_for_readiness(registered_handles) # parks once
for h in ready:
n = read(h, buf) # non-blocking, returns now
if n == WOULD_BLOCK: continue
handle(buf[0..n])go deeper
Be able to state the three modes plainly: blocking parks the thread, non-blocking returns "not yet", asynchronous finishes the work and notifies you.
Add why non-blocking is useless without readiness notification, and that the copy still runs on your thread in that model.
Bring in buffer ownership and memory scaling, the per-operation system-call cost, and the regular-file trap that stalls readiness-based loops.
Frame it as a choice of I/O substrate: batching and submission/completion rings versus readiness portability, and how that choice leaks into the whole server's buffer-management and backpressure strategy.
## The blocking baseline When a program asks the operating system for bytes from a socket, the default behaviour is that if no data has arrived the calling thread is put to sleep. It gives up the CPU, the scheduler runs something else, and the thread becomes runnable again only when data appears. From the program's point of view one line of code took as long as the network took. That is a *blocking* read, and it is why a server built this way needs one thread per concurrent conversation. ## Non-blocking: the call always returns immediately A handle can be switched into non-blocking mode. Now the same read either copies whatever bytes are already sitting in the kernel's receive buffer and returns that count, or it returns immediately with a distinct "would block" indication. The thread is never parked, so one thread can service many connections. The cost is that *you* now own the question of when to try again. Asking every handle in a loop (busy-polling) burns a whole core producing nothing, so in practice non-blocking handles are never used alone. They are used with a **readiness notification** facility: a call that takes a set of handles and blocks *once*, returning the subset that became readable or writable. One thread parks on that single call instead of on thousands of individual reads. The important structural point is that readiness only tells you the operation *would now succeed*. The data movement — the copy from kernel buffer into your memory — still executes on your thread, inside your event loop, when you get around to it. ## Asynchronous / completion-based I/O Completion-based I/O inverts that. You submit a descriptor of the work — "read up to 4 KB from this handle into *this* buffer" — and the call returns instantly having started nothing you can observe. Later you receive a completion event: how many bytes were transferred, or which error occurred. Between submission and completion the buffer belongs to the kernel: you must not free it, reuse it, or read from it. Windows I/O Completion Ports were the canonical example for decades; modern Linux submission/completion ring interfaces are another, and they add batching — many operations submitted with one system call. ## Why the distinction matters in practice 1. **Where the copy happens.** Readiness models pay one system call per ready event plus one per read; completion models can amortise both and let the kernel (or DMA hardware) move bytes while your thread does something else. 2. **Buffer lifetime and memory.** With readiness you can allocate the read buffer *after* being told data is waiting, so buffer memory scales with *active* connections. With completion the buffer is pinned for every operation in flight, so memory scales with *outstanding operations* — which for 100,000 idle-but-registered sockets is much worse unless the API lets you submit without a buffer. 3. **Files versus sockets.** Readiness models are a socket-and-pipe idea. A regular disk file is *always* reported ready, yet reading it can still park the thread on a disk seek. This is the single most common trap: an "event-driven" server that stalls its loop on file reads. Completion-based interfaces cover files properly, which is one reason they exist. 4. **Errors.** In a readiness model errors surface on the read you attempt; in a completion model they arrive attached to the completion record for the operation you submitted. ## Terms that get confused - *Non-blocking* is about the call not parking the thread. *Asynchronous* is about the work completing elsewhere and notifying you. *Concurrent* is about overlapping lifetimes of tasks. *Parallel* is about simultaneous execution on multiple cores. A single-threaded event-driven server is concurrent and non-blocking but not parallel. - Most language runtimes that market "async I/O" for networking are readiness-based underneath, with a completion-style API bolted on top. The programming model you see does not tell you which kernel mechanism is used. - "Non-blocking" in lock-free-algorithm literature means something else entirely (progress guarantees under contention). Context disambiguates. ## Mental model Readiness is a doorbell: it rings, you walk to the door and carry the parcel in yourself. Completion is a courier: you leave a box out, and you are notified once the parcel is already inside it.
- If a thread never blocks, why is a busy retry loop over non-blocking sockets still a bad design?Because the thread now spins at 100% CPU asking questions whose answer is almost always "nothing yet". You have replaced a cheap sleep with an expensive poll, stealing a core from real work and heating the machine for no throughput gain. Readiness notification exists so exactly one blocking wait covers thousands of handles, letting the thread sleep until something is genuinely actionable.
- Your event loop uses readiness notification and also reads configuration files during request handling. Why can that stall everything?Readiness interfaces treat regular files as permanently ready, so the file handle never signals "not yet" — but the actual read can still park the thread while the storage device seeks. One request touching a cold file therefore freezes the loop and every other connection it serves. File work belongs on a dedicated worker pool or on a true completion-based interface.
Readiness notification is a doorbell — it rings and you fetch the parcel yourself. Completion notification is a courier who already put the parcel in the box you left out and then texted you.
saying these in an interview costs you the question
- Saying non-blocking and asynchronous are the same thing.
- Claiming non-blocking I/O is faster per operation — it is not; it changes who waits, not how fast the network is.
- Assuming an event loop can safely read files because the handle reports ready.
- Thinking a single-threaded non-blocking server uses multiple cores.
- Believing the kernel copies the bytes for you in a readiness (reactor) model.