These are four ways a program can wait for input and output (I/O).
Blocking I/O: a call like read() puts the thread to sleep until data is ready. So one thread handles one connection at a time.
Non-blocking I/O: the descriptor has the O_NONBLOCK flag set. Now read() returns at once with EAGAIN when no data is ready. But checking every socket again and again in a loop wastes CPU.
I/O multiplexing solves that problem. One thread asks the kernel to watch many descriptors. The thread sleeps until at least one of them is ready. The calls for this are:
selectandpollepollon Linuxkqueueon BSD and macOS
The thread then does non-blocking reads on the ready descriptors only. This is the readiness model. Node's libuv uses it for sockets.
Asynchronous I/O goes one step further. You submit an operation, and the kernel does the whole thing. It tells you when the data is already in your buffer. Examples are io_uring on Linux and IOCP on Windows. This is the completion model.
In short: readiness means "you can read now". Completion means "the read is done".
- Blocking I/O: the thread sleeps until the operation completes.
- Non-blocking I/O: the call returns EAGAIN at once when data is not ready.
- Multiplexing: one thread waits on many file descriptors with select, poll, epoll or kqueue.
- Async I/O: the kernel does the operation and tells you when it is complete (io_uring, IOCP).
- Node's libuv uses readiness-based multiplexing for sockets.