Blocking vs. Nonblocking Operations
A blocking operation suspends the calling OS thread inside the operating-system kernel until the requested data or resource is ready. A nonblocking operation (O_NONBLOCK on Unix, overlapped/IOCP on Windows) returns control to the caller immediately: if the operation can complete right away, it returns Ok(bytes); if the kernel buffer is not yet ready, it returns Err(io::ErrorKind::WouldBlock) without putting the thread to sleep.
Kernel Readiness Notification (epoll, kqueue, IOCP, io_uring)
Instead of spinning in a tight CPU loop calling read() while it returns WouldBlock, nonblocking I/O pairs nonblocking sockets with an OS event notification reactor (epoll on Linux, kqueue on macOS/BSD, IOCP on Windows):
Nonblocking Socket + OS Event Reactor Architecture:
1. Worker calls `socket.read(&mut buf)`:
+-- Kernel buffer empty! Returns `Err(WouldBlock)` in nanoseconds.
+-- Task registers its `Waker` with the Reactor for `socket` readability
and returns `Poll::Pending` to the Executor.
2. Executor runs other ready tasks on the same thread.
3. Network packet arrives at NIC -> OS kernel marks `socket` readable:
+-- Reactor (`epoll_wait` / `kevent`) receives readiness event for `socket`
+-- Reactor calls `waker.wake()`, placing the Task back on the Executor's ready queue!
4. Executor polls Task -> `socket.read(&mut buf)` now returns `Ok(n_bytes)`!
Comparing Blocking and Nonblocking I/O Calls
| Property | Blocking Mode (Default in std::net) |
Nonblocking Mode (.set_nonblocking(true)) |
|---|---|---|
| Return when no data is available | Parks OS thread in kernel until data arrives | Immediately returns Err(io::ErrorKind::WouldBlock) |
| Return when send buffer is full | Parks OS thread until peer ACKs data | Immediately returns Err(io::ErrorKind::WouldBlock) |
TcpListener::accept() with no client |
Parks OS thread until a client connects | Immediately returns Err(io::ErrorKind::WouldBlock) |
| Used by | Synchronous thread-per-connection servers | Async runtimes (tokio, mio, smol) and event loops |
Demonstrating set_nonblocking(true) and ErrorKind::WouldBlock in std::net
Every socket type in std::net (TcpListener, TcpStream, UdpSocket) provides .set_nonblocking(true) to switch the underlying OS socket descriptor into nonblocking mode:
use std::io::{self, Read, Write};
use std::net::{TcpListener, TcpStream};
fn main() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
// Switch the listener into nonblocking mode:
listener.set_nonblocking(true)?;
// 1. Calling `.accept()` when no client has connected returns `WouldBlock` immediately:
match listener.accept() {
Ok(_) => println!("unexpected connection"),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
println!("listener.accept() returned WouldBlock immediately (no client yet)");
}
Err(e) => return Err(e),
}
// 2. Connect a client and accept it:
let mut client = TcpStream::connect(addr)?;
let (mut server_stream, _) = listener.accept()?;
server_stream.set_nonblocking(true)?;
// 3. Reading from `server_stream` before `client` writes returns `WouldBlock`:
let mut buf = [0_u8; 16];
println!(
"read before client write -> WouldBlock: {}",
server_stream.read(&mut buf).unwrap_err().kind() == io::ErrorKind::WouldBlock
);
// 4. After client writes, nonblocking read succeeds immediately:
client.write_all(b"ping")?;
loop {
match server_stream.read(&mut buf) {
Ok(n) => {
println!("nonblocking read succeeded: {:?}", &buf[..n]);
break;
}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => std::thread::yield_now(),
Err(e) => return Err(e),
}
}
Ok(())
}
listener.accept() returned WouldBlock immediately (no client yet)
read before client write -> WouldBlock: true
nonblocking read succeeded: [112, 105, 110, 103]