liveBook · Manning

I/O multiplexing

As explained in Grokking Concurrency

I/O multiplexing is an operating-system technique that monitors multiple I/O resources and reports which are ready for processing.

I/O multiplexing lets an operating system monitor multiple I/O resources, queue their events, and report which sockets are ready. The application can wait without busy-waiting, then process several connections through one thread. section 11.4

The basic flow is:

  • Register sockets and the read or write events they may produce. section 11.4
  • Block in select until at least one registered socket becomes ready, or return immediately when events are already waiting. section 11.4
  • Process each ready socket, then return to the event loop so other connections can be handled. section 11.4
  • Use platform-specific alternatives such as poll, epoll, kqueue, IOCP, or /dev/poll when appropriate. section 11.4

A single-threaded pizza server

An event-driven server registers its listening socket and client sockets, waits in select, and dispatches ready events to callbacks such as _on_accept, _on_read, and _on_write. Each callback handles one event type before control returns to the loop, allowing multiple clients to be served without one thread per client. section 11.5

# Chapter 11/pizza_reactor.py
class EventLoop:
    def __init__(self) -> None:
        self.writers = {}                                          #1
        self.readers = {}                                          #1

    def register_event(self, source: socket, event: Mask,
                       action: Action) -> None:
        key = source.fileno()                                      #2
        if event & select.POLLIN:                                  #3
            self.readers[key] = (source, event, action)            #3
        elif event & select.POLLOUT:                               #4
            self.writers[key] = (source, event, action)            #4

    def unregister_event(self, source: socket) -> None:            #5
        key = source.fileno() 
        if self.readers.get(key): 
            del self.readers[key]
        if self.writers.get(key): 
            del self.writers[key]

    def run_forever(self) -> None:
        while True:                                                #6
            readers, writers, _ = select.select(                   #6
                self.readers, self.writers, [])                    #6
            for reader in readers:                                 #7
                source, event, action = self.readers.pop(reader)   #7
                action(source)                                     #7

            for writer in writers:                                 #8
                source, event, action = self.writers.pop(writer)   #8
                action, msg = action                               #8
                action(source, msg)                                #8

Inspect the event-loop listing to see readers and writers registered by socket descriptor and dispatched through the readiness mechanism. section 11.5

How Grokking Concurrency covers it

How deep it goes
Explains readiness, blocking select, event queues, callbacks, and platform alternatives, stopping short of deeper implementation details.
Examples
Uses a single-threaded pizza server with server and client sockets plus accept, read, and write callbacks.
What it assumes
Assumes socket-based server concepts while introducing event loops, readiness notification, and callback-driven processing.
What you can do after
Build a single-threaded concurrent server by registering socket events and dispatching readiness notifications to callbacks.
How it connects
Connects nonblocking I/O with event loops, callbacks, synchronous event demultiplexers, and the reactor pattern.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of Grokking Concurrency. The section links point to the parts of the book it draws on.