the reactor versus the proactor pattern
/ reactor -> ree-AK-tor; proactor -> proh-AK-tor /
These are the two classic design patterns for structuring a server that handles many connections with few threads, and they line up exactly with the readiness and completion models. A reactor reacts to readiness: 'this socket can be read now,' and your handler then does the reading. A proactor acts ahead: it starts the I/O, and the OS completes the whole operation and then calls your handler with the result already in hand.
Reactor pattern, step by step: a synchronous event demultiplexer (epoll, kqueue, select) waits on many descriptors; when some become ready it dispatches each to a registered event handler; the handler performs the actual non-blocking read or write itself, then returns. The classic shape is the event loop plus a table of callbacks. It is readiness-based, so the handler does the data movement. Proactor pattern, step by step: you submit an asynchronous operation (read into this buffer); the OS performs the read in the background; a completion demultiplexer (IOCP, io_uring's completion queue) waits for finished operations; when one completes, it dispatches to a completion handler that receives the already-filled buffer. It is completion-based, so the OS did the data movement before your handler ran.
Why it matters: this vocabulary lets you reason about and port server designs across platforms. Reactor maps onto epoll (Linux) and kqueue (BSD/macOS); proactor maps onto IOCP (Windows) and io_uring (Linux). Cross-platform libraries often present a proactor-style API (you say 'read this' and get a callback when done) and implement it with a real proactor on Windows but emulate it over a reactor on Linux - or, increasingly, use io_uring to get a genuine proactor there too. The patterns are about WHO performs the I/O, not merely about using non-blocking sockets.
/* reactor */ on_readable(fd): n = read(fd, buf, len); process(buf, n); /* you read */ /* proactor */ start_read(fd, buf, len); ... on_complete(buf, n): process(buf, n); /* OS read */
In a reactor your handler does the read on readiness; in a proactor the OS finishes the read, then calls your handler.
The defining difference is who moves the bytes: in a reactor the application does the I/O after a readiness signal; in a proactor the kernel does the I/O and signals completion. A non-blocking reactor is still synchronous I/O - only the proactor is truly asynchronous.