An I/O selector lets one thread wait for activity across many file descriptors instead of dedicating one blocked thread to each connection.
Recommended reading order
The three families differ by where the application waits. A blocking thread waits inside the operation, a readiness API waits before the operation, and a completion API waits after submitting the operation.
Blocking I/O
Blocking I/O means the calling thread enters a system call and does not return until the operation can complete, fails, or is interrupted. A thread reading from a TCP socket can sleep until the peer sends bytes. A thread accepting connections can sleep until a client connects.
The programming model is simple because local variables and the call stack hold progress. The concurrency model is expensive when many operations are waiting: each blocked connection usually needs a thread or process that the kernel scheduler must track.
Readiness-Based I/O
Readiness-based I/O asks the kernel which file descriptors are ready for an operation. “Ready” does not mean the operation has already happened. It means the application should be able to call read, write, accept, or a similar operation without blocking immediately.
| API | Platform | Notes |
|---|---|---|
select | POSIX | Uses fixed-size descriptor sets and requires rebuilding/scanning sets on each call. |
poll | POSIX | Uses an array of pollfd records, avoiding select’s fixed bitset shape but still scans linearly. |
| epoll | Linux | Keeps an in-kernel interest list and ready list, which scales better for large descriptor sets. |
kqueue | BSD and macOS | Uses filters for file descriptors, timers, signals, and other event sources. |
The event-loop shape is:
- Register descriptors and requested events.
- Wait for a batch of readiness notifications.
- Run non-blocking operations on the ready descriptors.
- Store any unfinished progress in per-connection state.
- Return to the selector.
Readiness APIs are strongest for sockets, pipes, timers, signals, and other descriptors whose readiness changes over time. They are not universal asynchronous file-I/O APIs.
Completion-Based I/O
Completion-based I/O asks the kernel to perform operations and later report which ones completed.
| API | Platform | Notes |
|---|---|---|
| io_uring | Linux | Uses shared submission and completion rings for many operation types, including socket and regular-file operations. |
| I/O Completion Ports (IOCP) | Windows | A completion mechanism for associating completed I/O with worker threads. |
The event-loop shape is:
- Submit one or more operations.
- Wait for completion events.
- Match each completion to application state.
- Submit follow-up operations.
Completion APIs move more of the operation lifecycle into the kernel interface. The application still owns buffers, state, ordering constraints, cancellation, and backpressure.
Selector Threads And Worker Pools
Many runtimes combine a selector thread with a worker pool. The selector thread handles readiness or completion events. Worker threads handle CPU-bound work or blocking operations that the selector cannot safely perform inline.
This split exists because an event loop is only responsive while event handlers stay short. If an event handler performs a cold disk read, compression pass, database query, or long calculation, every other connection assigned to that loop waits behind it.
The older Tokio 0.1 scheduler used this broad shape: an I/O driver waited for readiness and tasks were scheduled around that driver.
Choosing A Model
| Model | Use when | Watch out for |
|---|---|---|
| Blocking thread per connection | Simplicity matters more than very high concurrency. | Thread stacks, context switches, scheduler overhead. |
| Readiness selector | Many descriptors are mostly waiting on network or event readiness. | Explicit state machines and blocking filesystem/CPU work. |
| Completion interface | Many operations can be submitted concurrently and completions can be batched. | Queue depth, object lifetime, cancellation, and portability. |