selectors Module Complexity¶
The selectors module provides a high-level interface for multiplexed I/O, allowing monitoring of multiple sockets efficiently using system-level mechanisms (select, epoll, kqueue, IOCP).
Classes & Methods¶
| Operation | Time | Space | Notes |
|---|---|---|---|
DefaultSelector() |
O(1) | O(1) | Create selector |
selector.register(fileobj) |
O(1) | O(1) | Register socket/file |
selector.unregister(fileobj) |
O(1) | O(1) | Unregister |
selector.select(timeout) |
O(n) or O(k) | O(k) | select: O(n) scans all fds; epoll/kqueue: O(k) for ready fds |
selector.modify(fileobj) |
O(1) | O(1) | Change registration |
selector.get_map() |
O(1) | O(n) | Get all registrations |
Creating Selectors¶
Time Complexity: O(1)¶
import selectors
# Create selector: O(1)
# Uses best available mechanism (epoll, kqueue, select)
sel = selectors.DefaultSelector() # O(1)
# Or specify type explicitly
sel = selectors.EpollSelector() # O(1) on Linux
sel = selectors.KqueueSelector() # O(1) on BSD/macOS
sel = selectors.SelectSelector() # O(1) fallback
Space Complexity: O(1)¶
import selectors
# Selector object is small
sel = selectors.DefaultSelector() # O(1) space
Registering File Objects¶
Time Complexity: O(1)¶
import selectors
import socket
sel = selectors.DefaultSelector()
# Create socket
sock = socket.socket()
# Register socket: O(1)
# Register for read events
sel.register(sock, selectors.EVENT_READ, data=user_data) # O(1)
# Register for write events
sel.register(sock, selectors.EVENT_WRITE) # O(1)
# Register for both: O(1)
sel.register(sock, selectors.EVENT_READ | selectors.EVENT_WRITE) # O(1)
Space Complexity: O(1) per registration¶
import selectors
# Each registration stores minimal info: O(1)
sel.register(sock, selectors.EVENT_READ) # O(1) space
Selecting with Timeout¶
Time Complexity: O(k) for epoll/kqueue, O(n) for select¶
Where n = number of registered file objects, k = number of ready file objects. Note: epoll/kqueue return only ready events in O(k), while select must scan all n registered fds.
import selectors
import socket
sel = selectors.DefaultSelector()
# Register sockets: O(k) for k sockets
for i in range(100):
sock = socket.socket()
sel.register(sock, selectors.EVENT_READ) # O(1) per socket
# Wait for I/O: O(n) where n = registered sockets
# Returns only ready file objects: O(k) where k <= n
events = sel.select(timeout=1.0) # O(n) for select, O(k) for epoll/kqueue
# Process events: O(k)
for key, mask in events: # k = ready file objects
if mask & selectors.EVENT_READ:
data = key.fileobj.recv(4096) # Handle read
Space Complexity: O(k)¶
Where k = number of ready file objects.
import selectors
# select() returns only ready events
events = sel.select(timeout=1.0) # O(k) space for k ready
# Iterate ready events: O(k)
for key, mask in events:
process(key, mask) # O(k) total
Modifying Registrations¶
Time Complexity: O(1)¶
import selectors
import socket
sel = selectors.DefaultSelector()
sock = socket.socket()
# Register: O(1)
key = sel.register(sock, selectors.EVENT_READ) # O(1)
# Modify to watch writes instead: O(1)
sel.modify(sock, selectors.EVENT_WRITE) # O(1)
# Modify back to read: O(1)
sel.modify(sock, selectors.EVENT_READ) # O(1)
Space Complexity: O(1)¶
import selectors
# Modifying doesn't allocate new space
sel.modify(sock, selectors.EVENT_WRITE) # O(1) space
Unregistering¶
Time Complexity: O(1)¶
import selectors
# Unregister socket: O(1)
sel.unregister(sock) # O(1)
# Unregister by key: O(1)
key = sel.get_map()[sock]
sel.unregister(key) # O(1)
Space Complexity: O(1)¶
import selectors
# Cleanup is fast: O(1)
sel.unregister(sock) # O(1)
Common Patterns¶
Echo Server with Multiple Clients¶
import selectors
import socket
sel = selectors.DefaultSelector()
def accept(sock):
"""Accept new connection."""
conn, addr = sock.accept()
conn.setblocking(False)
# Register for read: O(1)
sel.register(conn, selectors.EVENT_READ, data=addr) # O(1)
def read(conn, addr):
"""Read data from client."""
data = conn.recv(4096)
if data:
# Echo back: could register for write
sel.modify(conn, selectors.EVENT_WRITE) # O(1) to modify
else:
# Close connection: O(1) to unregister
sel.unregister(conn) # O(1)
conn.close()
def write(conn):
"""Write data to client."""
# Send buffered data
conn.sendall(buffer)
# Switch back to read: O(1)
sel.modify(conn, selectors.EVENT_READ) # O(1)
def main():
"""Main event loop."""
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(('localhost', 9000))
server.listen()
server.setblocking(False)
# Register server socket for accepts: O(1)
sel.register(server, selectors.EVENT_READ, data=None) # O(1)
while True:
# Wait for I/O: O(k) for epoll/kqueue, O(n) for select
events = sel.select() # O(k) ready fds typically
for key, mask in events: # O(k) where k = ready
if key.data is None:
# Server socket ready
accept(key.fileobj) # O(1) to register
else:
# Client socket ready
if mask & selectors.EVENT_READ:
read(key.fileobj, key.data)
elif mask & selectors.EVENT_WRITE:
write(key.fileobj)
Non-blocking Socket Operations¶
import selectors
import socket
sel = selectors.DefaultSelector()
def connect_many(hosts_ports):
"""Connect to multiple hosts simultaneously."""
sockets = []
for host, port in hosts_ports:
sock = socket.socket()
sock.setblocking(False)
try:
sock.connect((host, port))
except BlockingIOError:
pass # Expected for non-blocking
# Register for write (connection completion): O(1)
sel.register(sock, selectors.EVENT_WRITE, data=(host, port))
sockets.append(sock)
# Wait for connections: O(n log n)
# n = number of sockets
while True:
events = sel.select() # O(n log n)
if not events:
break
for key, mask in events: # O(k) ready
sock = key.fileobj
try:
sock.getpeername() # Check if connected
print(f"Connected to {key.data}")
except OSError:
print(f"Failed to connect to {key.data}")
sel.unregister(sock) # O(1)
sock.close()
return sockets
File Object Monitoring¶
import selectors
import sys
sel = selectors.DefaultSelector()
def read_input():
"""Read from stdin."""
line = input()
return line
def main():
"""Monitor multiple inputs."""
# Register stdin: O(1)
sel.register(sys.stdin, selectors.EVENT_READ) # O(1)
# Register other files: O(1) each
with open('file.txt') as f:
sel.register(f, selectors.EVENT_READ) # O(1)
# Event loop: O(n log n) where n = files
while True:
events = sel.select() # O(n log n)
for key, mask in events: # O(k) ready
if key.fileobj == sys.stdin:
data = key.fileobj.readline()
process_input(data)
else:
data = key.fileobj.read(4096)
process_file(data)
Performance Characteristics¶
Selector Efficiency¶
import selectors
import select
import socket
# selectors uses best mechanism available
# On Linux (many sockets): epoll - O(1) per ready event, O(k) total
# On BSD/macOS: kqueue - O(1) per ready event, O(k) total
# Fallback: select - O(n) per wait where n = all registered
sel = selectors.DefaultSelector()
# For thousands of connections:
# select: becomes slow O(n) per select() - must scan all fds
# epoll/kqueue: stays fast O(k) per select() - returns only ready fds
# Registering: always O(1)
for i in range(10000):
sel.register(socks[i], selectors.EVENT_READ) # O(10000)
# Selecting: depends on mechanism
# epoll/kqueue: O(k) where k = ready fds (typically small)
# select: O(n) = O(10000) - must check all registered fds
events = sel.select() # Fast with epoll/kqueue, slow with select
Best Practices¶
import selectors
import socket
sel = selectors.DefaultSelector()
# Good: Use DefaultSelector (picks best mechanism)
sel = selectors.DefaultSelector() # O(1)
# Good: Avoid blocking operations in select loop
while True:
events = sel.select(timeout=1.0) # Don't block
for key, mask in events:
# Non-blocking operations only
data = key.fileobj.recv(4096) # Will not block
process(data)
# Avoid: Blocking calls in event loop
while True:
events = sel.select()
for key, mask in events:
# This might block!
data = key.fileobj.recv(4096*1024) # Could block
# Good: Use with asyncio or concurrent.futures for CPU work
import concurrent.futures
executor = concurrent.futures.ThreadPoolExecutor(max_workers=4)
while True:
events = sel.select()
for key, mask in events:
# CPU-intensive work in thread pool
future = executor.submit(process, data)
# Good: Set non-blocking mode
sock = socket.socket()
sock.setblocking(False) # Required for selector use
sel.register(sock, selectors.EVENT_READ)
# Avoid: Mixing blocking and non-blocking
sock.setblocking(True) # Will block select()!
Scalability¶
import selectors
# Thousands of connections
connections = 10000
sel = selectors.DefaultSelector()
# Register all: O(n)
for i in range(connections):
sel.register(socks[i], selectors.EVENT_READ) # O(10000)
# Event loop: scales well with epoll/kqueue
# O(k) where k = ready events (typically small)
while True:
# Usually only few sockets ready at once
# If 100 ready: O(100) with epoll/kqueue vs O(10000) with select
events = sel.select() # Efficient with epoll/kqueue!
for key, mask in events: # Only processes ready events
handle(key, mask)
Version Notes¶
- Python 3.4+: selectors module introduced
- Python 3.5+: Enhanced performance
- Python 3.10+: Better Windows support
Comparison with Alternatives¶
# selectors: High-level, portable
import selectors
sel = selectors.DefaultSelector()
# select: Low-level, limited to ~1024 fds
import select
select.select(rlist, wlist, xlist, timeout)
# asyncio: Better for cooperative multitasking
import asyncio
await asyncio.wait_for(coro, timeout)
# Use selectors for:
# - Efficient handling of many connections
# - Mixing different I/O types
# - Simple non-blocking servers
Related Documentation¶
- asyncio Module - Modern async/await alternative
- socket Module - Low-level socket operations
- concurrent.futures Module - For CPU-intensive work