Asynchronous Rust

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]