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.
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
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.