liveBook · Manning

Non-blocking I/O

As explained in Grokking Concurrency

Nonblocking I/O is an input/output technique in which an operation returns control without waiting for its external operation to complete.

Imagine checking whether a pizza is ready, then doing something else while it cooks. Non-blocking I/O works similarly: an I/O call returns without making the execution thread wait for a device or network connection. The application can work on another task and try again when the I/O is ready. section 10.11

This matters when I/O takes much longer than CPU work. A server waiting for one client's network data could instead attend to another client. One thread can therefore make progress on multiple I/O-bound requests without assigning a waiting thread to each connection. It does not make an individual I/O operation complete sooner. section 10.0 section 10.12

A socket that is not ready

In Python, setblocking(False) puts a socket in non-blocking mode. A call such as recv() cannot wait for data: if none is available, it raises BlockingIOError. The application can handle that exception, serve another connection, and retry later. The exception means “not ready now,” not necessarily “the connection has failed.” section 10.11

# Chapter 10/pizza_busy_wait.py
import typing as T
from socket import socket, create_server

BUFFER_SIZE = 1024
ADDRESS = ("127.0.0.1", 12345)

class Server:
    clients: T.Set[socket] = set()

    def __init__(self) -> None:
        try:
            print(f"Starting up at: {ADDRESS}")
            self.server_socket = create_server(ADDRESS)
            self.server_socket.setblocking(False)                    #1
        except OSError:
            self.server_socket.close()
            print("\nServer stopped.")

    def accept(self) -> None:
        try:
            conn, address = self.server_socket.accept()
            print(f"Connected to {address}")
            conn.setblocking(False)                                  #1
            self.clients.add(conn)
        except BlockingIOError:                                      #2
            pass                                                     #2

    def serve(self, conn: socket) -> None:
        try:
            while True:
                data = conn.recv(BUFFER_SIZE)
                if not data:
                    break
                try:
                    order = int(data.decode())
                    response = f"Thank you for ordering {order} pizzas!\n"
                except ValueError:
                    response = "Wrong number of pizzas, please try again\n"
                print(f"Sending message to {conn.getpeername()}")
                conn.send(response.encode())
        except BlockingIOError:                                      #2
            pass                                                     #2

    def start(self) -> None:
        print("Server listening for incoming connections")
        try:
            while True:
                self.accept()
                for conn in self.clients.copy():
                    self.serve(conn)

        finally:
            self.server_socket.close()
            print("\nServer stopped.")

if __name__ == "__main__":
    server = Server()
    server.start()

The server listing puts its listening and accepted sockets in non-blocking mode and handles BlockingIOError when accept() has no connection ready. section 10.11

Repeatedly retrying every socket in an endless loop is called busy-waiting. It avoids blocking but can waste processor time checking for work that is not there. A reactor-style approach instead uses a notification call such as select to wait for I/O status, then handles sockets that are ready. section 11.10 section 11.11

Non-blocking and asynchronous describe different things. Non-blocking describes how an I/O call returns; asynchronous describes how the application continues and later obtains a result. A non-blocking call may require repeated readiness checks. In an asynchronous non-blocking model, an initiated operation can instead finish in the background and produce a signal or callback. section 11.8 section 11.12

How Grokking Concurrency covers it

How deep it goes
Explains socket mode, retry behavior, busy-waiting, and notification alternatives, without detailing device-level implementation.
Examples
Uses a pizza-readiness analogy and a Python socket server that handles BlockingIOError.
What it assumes
Introduces the waiting problem and non-blocking behavior from first principles, then uses Python socket calls for the implementation.
What you can do after
Apply it via Python sockets to handle multiple I/O-bound connections while recognizing when repeated polling wastes CPU time.
How it connects
Connects non-blocking sockets to busy-waiting, the reactor pattern, select, and asynchronous I/O.

Also explained in

Compare how 5 books explain non-blocking I/O →

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.