Fundamentals 19 min read

Understanding I/O Multiplexing in Python: select, poll, epoll, and selectors

This article explains how to use Python's socket module together with I/O multiplexing techniques such as select, poll, epoll, and the high‑level selectors module to build non‑blocking, scalable network servers that can handle multiple client connections concurrently.

Python Programming Learning Circle
Python Programming Learning Circle
Python Programming Learning Circle
Understanding I/O Multiplexing in Python: select, poll, epoll, and selectors

1. Programming Fundamentals

Real program = I/O multiplexing + non‑blocking mode.

Why it works

Operating systems can notify you when a socket becomes readable or writable, allowing you to call read only when data is available.

The OS provides system calls for I/O multiplexing that can monitor many file descriptors in a single thread, enabling concurrent handling of I/O.

Typical scenarios for I/O multiplexing

Interactive input combined with network sockets.

A client handling multiple sockets simultaneously.

A TCP server that needs to monitor both the listening socket and established connections.

A service that handles both TCP and UDP.

Servers that need to manage multiple services or protocols.

Core advantages

I/O multiplexing incurs far less overhead than creating multiple processes or threads because a single thread can reuse the same resources.

System calls that support I/O multiplexing

select : limited to 1024 descriptors by default; each call copies the descriptor set between user and kernel space, which can be costly.

poll : removes the descriptor‑count limit but still scans linearly.

epoll (Linux): handles a large number of descriptors efficiently, supports edge‑triggered and level‑triggered modes, and avoids repeated copying by using a ready‑list.

kqueue (FreeBSD): similar to epoll for BSD systems.

2. Implementing I/O Multiplexing

select + poll + epoll

Multi‑client demo

<code>import socket
HOST = '127.0.0.1'
PORT = 8001
messages = [
    'This is ',
    'the message. ',
    'It will be sent ',
    'in parts.'
]
socks = [
    socket.socket(socket.AF_INET, socket.SOCK_STREAM),
    socket.socket(socket.AF_INET, socket.SOCK_STREAM),
]
print(f'connecting to {HOST} port {PORT}')
for s in socks:
    s.connect((HOST, PORT))
for index, message in enumerate(messages):
    _, is_odd = divmod(index, 2)
    outgoing_data = message.encode()
    for index, s in enumerate(socks):
        if divmod(index, 2)[1] != is_odd:
            continue
        print(f'{s.getsockname()}: sending {outgoing_data}')
        s.send(outgoing_data)
    for index, s in enumerate(socks):
        if divmod(index, 2)[1] != is_odd:
            continue
        data = s.recv(1024)
        print(f'{s.getsockname()}: received {data}')
        if not data:
            s.close()
</code>

Running the server with select :

<code>Server start at: 127.0.0.1:8001
Connected by ('127.0.0.1', 50442)
Connected by ('127.0.0.1', 50443)
received "b'This is '" from ('127.0.0.1', 50442)
received "b'the message. '" from ('127.0.0.1', 50443)
received "b'It will be sent '" from ('127.0.0.1', 50442)
received "b'in parts.'" from ('127.0.0.1', 50443)
</code>

Running the client:

<code>connecting to 127.0.0.1 port 8001
('127.0.0.1', 50442): sending b'This is '
('127.0.0.1', 50442): received b"Server received b'This is '"
('127.0.0.1', 50443): sending b'the message. '
('127.0.0.1', 50443): received b"Server received b'the message. '"
('127.0.0.1', 50442): sending b'It will be sent '
('127.0.0.1', 50442): received b"Server received b'It will be sent '"
('127.0.0.1', 50443): sending b'in parts.'
('127.0.0.1', 50443): received b"Server received b'in parts.'"
</code>

select implementation details

Three lists are returned: readable, writable, exceptional.

New connections are accepted and added to the input list; data is read and stored in per‑connection queues.

When a socket becomes writable, queued messages are sent.

Exceptional sockets are removed and closed.

Limitations: default FD limit (1024) and linear scanning cause high CPU usage under many connections.

poll implementation details

Uses a class that registers sockets with flags such as POLLIN , POLLPRI , POLLHUP , POLLERR , POLLNVAL .

Handles readable, writable, and hang‑up events similarly to select , but still suffers from linear scanning.

Provides level‑triggered notifications; events are not lost but can cause repeated copies.

epoll implementation details

Scales to a very large number of descriptors; supports edge‑triggered and level‑triggered modes.

Registers interest events; when a descriptor is ready, it is placed on a ready list, avoiding full scans.

Copies descriptor information to the kernel only once per registration, reducing overhead.

3. Using the selectors module

The selectors module (Python 3.4+) provides a high‑level wrapper around the underlying I/O multiplexing mechanisms, automatically choosing the most efficient one for the platform.

Available selector classes

SelectSelector – uses select

PollSelector – uses poll

EpollSelector – uses epoll (Linux)

DevpollSelector – rarely used

KqueueSelector – uses kqueue (BSD)

Event flags

EVENT_READ – socket is readable

EVENT_WRITE – socket is writable

Example with selectors

<code>import socket
import selectors
from queue import Queue, Empty
HOST = '127.0.0.1'
PORT = 8001
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(0)
sock.bind((HOST, PORT))
sock.listen(5)
sel = selectors.DefaultSelector()
message_queues = {}
print(f'Server start at: {HOST}:{PORT}')
sel.register(sock, selectors.EVENT_READ | selectors.EVENT_WRITE)
while True:
    for key, mask in sel.select(timeout=0.5):
        conn = key.fileobj
        if conn is sock:
            conn, addr = sock.accept()
            print(f'Connected by {addr}')
            conn.setblocking(0)
            message_queues[conn] = Queue()
            sel.register(conn, selectors.EVENT_READ | selectors.EVENT_WRITE)
        elif mask & selectors.EVENT_READ:
            data = conn.recv(1024)
            if data:
                print(f'received "{data}" from {conn.getpeername()}')
                message_queues[conn].put(data)
        elif mask & selectors.EVENT_WRITE:
            try:
                next_msg = message_queues[conn].get_nowait()
            except Empty:
                pass
            else:
                conn.send(bytes(f'Server received {next_msg}', 'utf-8'))
                sel.modify(sock, selectors.EVENT_READ)
</code>

Callback‑style final example

<code>import socket
import selectors
from queue import Queue, Empty
HOST = '127.0.0.1'
PORT = 8001
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(0)
sock.bind((HOST, PORT))
sock.listen(5)
sel = selectors.DefaultSelector()
print(f'Server start at: {HOST}:{PORT}')

def read(conn, mask):
    data = conn.recv(1024)
    if data:
        print(f'received "{data}" from {conn.getpeername()}')
        conn.send(bytes(f'Server received {data}', 'utf-8'))
    else:
        sel.unregister(conn)
        conn.close()

def accept(sock, mask):
    conn, addr = sock.accept()
    print(f'Connected by {addr}')
    conn.setblocking(0)
    sel.register(conn, selectors.EVENT_READ, read)

sel.register(sock, selectors.EVENT_READ, accept)
while True:
    events = sel.select(0.5)
    for key, mask in events:
        callback = key.data
        callback(key.fileobj, mask)
</code>

Original article: https://www.escapelife.site/posts/5e76085b.html

I/O multiplexingnetwork programmingepollselectSocket
Python Programming Learning Circle
Written by

Python Programming Learning Circle

A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.