尚未翻译
本页面尚未翻译成中文,因此显示英文原文。 帮助翻译
selectors Module Complexity¶
The selectors module wraps the select module's primitives - select(), poll(), epoll,
/dev/poll and kqueue - behind one interface: register file objects with the events to watch,
then ask which of them are ready. Registration is a dict entry keyed by file descriptor, plus at
most a couple of system calls; the cost that differs between the implementations is select()
itself.
n is the file objects registered with a selector, and k is the (key, events) pairs one
select() call returns - one per ready file object, or two for a file kqueue reports ready for
reading and writing separately. The bounds price the work of one call, not the time it spends waiting for
a descriptor to become ready, and treat a system call that adds, changes or removes one
descriptor as O(1). A file object is an integer descriptor or anything with a fileno() method,
which every method that takes one calls to find its registration.
Complexity Reference¶
Selector classes¶
| Operation | Time | Space | Notes |
|---|---|---|---|
selectors.DefaultSelector() |
O(1) | O(1) | An alias chosen at import: the first of kqueue, epoll, /dev/poll, poll and select that works; EpollSelector on Linux. Where it is DevpollSelector, it costs what that costs |
selectors.SelectSelector() |
O(1) | O(1) | Available everywhere; on Unix, descriptors must be below FD_SETSIZE (usually 1024) or select() raises ValueError |
selectors.PollSelector() |
O(1) | O(1) | Unix |
selectors.EpollSelector() |
O(1) | O(1) | Linux; opens an epoll descriptor |
selectors.KqueueSelector() |
O(1) | O(1) | BSD and macOS; opens a kqueue descriptor |
selectors.DevpollSelector() |
O(1) | O(L) | Solaris; L = the process's open-file limit, one slot per descriptor reserved up front |
EpollSelector.fileno(), KqueueSelector.fileno(), DevpollSelector.fileno() |
O(1) | O(1) | The kernel object's own descriptor |
BaseSelector¶
| Operation | Time | Space | Notes |
|---|---|---|---|
selectors.BaseSelector |
O(1) | O(1) | Abstract; a subclass supplies register, unregister, select and get_map, and inherits modify as unregister plus register |
BaseSelector.register(fileobj, events, data=None) |
O(1) | O(1) | Returns the new SelectorKey; KeyError if the descriptor is already registered, ValueError if events is empty or has unknown bits |
BaseSelector.unregister(fileobj) |
O(1) | O(1) | O(n) once fileobj is closed: when fileno() fails, every registration is scanned for the object |
BaseSelector.modify(fileobj, events, data=None) |
O(1) | O(1) | Returns the updated SelectorKey; changing only data makes no system call |
BaseSelector.select(timeout=None) |
O(n) or O(k) | O(k) to O(n) | Returns k (key, events) pairs; the cost is the implementation's, below. timeout <= 0 polls and None blocks |
BaseSelector.get_key(fileobj) |
O(1) | O(1) | KeyError if not registered, RuntimeError once the selector is closed |
BaseSelector.get_map() |
O(1) | O(1) | The same read-only view on every call, not a copy; None once closed |
len(mapping), mapping[fileobj], fileobj in mapping |
O(1) | O(1) | On the view get_map() returns |
| Iterating the view | O(n) | O(1) | Yields file descriptors, not file objects; .values() yields the keys |
BaseSelector.close(), leaving a with block |
O(n) | O(1) | Drops every registration and closes the kernel object, if there is one; the registered files stay open |
select() by implementation¶
| Operation | Time | Space | Notes |
|---|---|---|---|
SelectSelector.select(timeout=None) |
O(n) | O(n) | Every registered descriptor is copied into the select() call and checked on return |
PollSelector.select(timeout=None) |
O(n) | O(k) | The kernel walks the whole n-entry array the selector keeps between calls; the first call after registrations change rebuilds it, resizing it to n |
EpollSelector.select(timeout=None) |
O(k) | O(n) | The kernel hands back only ready descriptors, but each call allocates a result buffer sized for all n |
KqueueSelector.select(timeout=None) |
O(k) | O(n) | As epoll |
DevpollSelector.select(timeout=None) |
O(k) | O(k) | Reuses the array allocated at construction |
SelectorKey and constants¶
| Operation | Time | Space | Notes |
|---|---|---|---|
selectors.SelectorKey |
O(1) | O(1) | A named tuple: modify replaces a registration's key rather than changing it in place |
SelectorKey.fileobj, SelectorKey.fd, SelectorKey.events, SelectorKey.data |
O(1) | O(1) | What was registered, its descriptor, the event mask, and the attached object |
selectors.EVENT_READ, selectors.EVENT_WRITE |
O(1) | O(1) | Bit flags, combined with bitwise or to watch both |
Choosing a Selector¶
DefaultSelector is not a class of its own but whichever implementation the platform supports
best, and all of them share the BaseSelector interface. Use it as a context manager so the
kernel object is released.
import selectors
assert selectors.DefaultSelector in (
selectors.SelectSelector,
getattr(selectors, 'PollSelector', None),
getattr(selectors, 'EpollSelector', None),
getattr(selectors, 'DevpollSelector', None),
getattr(selectors, 'KqueueSelector', None),
)
with selectors.DefaultSelector() as sel: # O(1)
assert isinstance(sel, selectors.BaseSelector)
assert sel.get_map() is not None
assert sel.get_map() is None # O(n) close on exit
Registering File Objects¶
register() stores a SelectorKey under the file's descriptor and returns it. A descriptor can
be registered once; to watch different events on it, modify() the registration.
import selectors
import socket
left, right = socket.socketpair()
with selectors.DefaultSelector() as sel:
key = sel.register(left, selectors.EVENT_READ, data='left') # O(1)
assert key.fileobj is left and key.fd == left.fileno()
assert sel.get_key(left) is key # O(1)
try:
sel.register(left, selectors.EVENT_WRITE)
except KeyError as error:
assert 'already registered' in str(error)
else:
raise AssertionError('a descriptor was registered twice')
try:
sel.register(right, 0)
except ValueError as error:
assert 'Invalid events' in str(error)
else:
raise AssertionError('an empty event mask was accepted')
both = selectors.EVENT_READ | selectors.EVENT_WRITE
changed = sel.modify(left, both, data='left') # O(1)
assert changed.events == both and key.events == selectors.EVENT_READ
renamed = sel.modify(left, both, data='renamed') # O(1) - no system call
assert sel.get_key(left) is renamed
assert sel.unregister(left) is renamed # O(1)
left.close()
right.close()
What select() Returns¶
select() returns a list of (key, events) pairs for the ready file objects, where events is
the ready events masked by what was registered. Iterating the result costs O(k) whatever the
implementation. kqueue reports reading and writing as separate pairs, so a file watched for both
can appear twice.
import selectors
import socket
left, right = socket.socketpair()
with selectors.DefaultSelector() as sel:
sel.register(left, selectors.EVENT_READ, data='reader')
assert sel.select(timeout=0) == [] # nothing to read yet
right.send(b'ping')
ready = sel.select(timeout=0) # O(k) for epoll and kqueue, O(n) for poll and select
assert len(ready) == 1
key, events = ready[0]
assert key.data == 'reader' and events == selectors.EVENT_READ
assert key.fileobj.recv(4) == b'ping'
left.close()
right.close()
How select() Scales¶
select() and poll() hand the kernel every registered descriptor on every call and check each
on return, so a call costs O(n) however few are ready. epoll, kqueue and /dev/poll keep the
interest set in the kernel and return only the ready descriptors, so a call costs O(k). For many
mostly idle connections that is the difference that matters, and it is why DefaultSelector
prefers them.
import os
import selectors
pipes = [os.pipe() for _ in range(200)]
os.write(pipes[-1][1], b'x') # one of 200 is ready
candidates = [selectors.SelectSelector]
for name in ('PollSelector', 'EpollSelector', 'KqueueSelector', 'DevpollSelector'):
if hasattr(selectors, name):
candidates.append(getattr(selectors, name))
for cls in candidates:
with cls() as sel:
for r, _ in pipes:
sel.register(r, selectors.EVENT_READ) # O(1) each
ready = sel.select(timeout=0) # O(n) for select/poll, O(k) for the rest
assert [key.fd for key, _ in ready] == [pipes[-1][0]]
for r, w in pipes:
os.close(r)
os.close(w)
The kernel-side selectors are not free of n: epoll and kqueue allocate a result buffer sized for
every registration on each call, so their select() is O(k) in time and O(n) in memory.
Unregistering Before Closing¶
Registrations are found by descriptor. Once a file is closed, its fileno() fails, and
unregister(), modify() and get_key() fall back to scanning every registration for the object
itself - O(n) per call. Unregister first.
import selectors
import socket
with selectors.DefaultSelector() as sel:
left, right = socket.socketpair()
sel.register(left, selectors.EVENT_READ)
sel.unregister(left) # O(1) - fileno() still works
left.close()
sel.register(right, selectors.EVENT_READ)
right.close()
assert right.fileno() == -1
sel.unregister(right) # O(n) - found by scanning every registration
assert len(sel.get_map()) == 0
The Registration Map¶
get_map() returns a live, read-only view of the registrations, not a snapshot, so holding it
costs nothing and it always reflects the current state. Lookups take a file object; iteration
yields descriptors.
import selectors
import socket
left, right = socket.socketpair()
with selectors.DefaultSelector() as sel:
mapping = sel.get_map() # O(1)
assert sel.get_map() is mapping
sel.register(left, selectors.EVENT_READ, data='left')
sel.register(right, selectors.EVENT_WRITE, data='right')
assert len(mapping) == 2 # O(1) - the view sees the new entries
assert mapping[left].data == 'left' and right in mapping # O(1)
assert set(mapping) == {left.fileno(), right.fileno()} # O(n)
assert {key.data for key in mapping.values()} == {'left', 'right'} # O(n)
sel.close()
assert sel.get_map() is None
try:
sel.get_key(left)
except RuntimeError as error:
assert 'closed' in str(error)
else:
raise AssertionError('a closed selector answered')
left.close()
right.close()
Common Patterns¶
Dispatching on Attached Data¶
Attaching a callback as data means each ready event carries its own handler, so the loop does
O(k) work on top of whatever select() costs.
import selectors
import socket
received = []
def on_readable(conn):
received.append(conn.recv(1024))
pairs = [socket.socketpair() for _ in range(3)]
with selectors.DefaultSelector() as sel:
for left, _ in pairs:
left.setblocking(False)
sel.register(left, selectors.EVENT_READ, data=on_readable) # O(1)
pairs[0][1].send(b'a')
pairs[2][1].send(b'c')
for key, events in sel.select(timeout=0): # k = 2 ready
key.data(key.fileobj) # O(1) dispatch per ready file
for left, right in pairs:
sel.unregister(left) # O(1) - before closing
left.close()
right.close()
assert sorted(received) == [b'a', b'c']
Performance Best Practices¶
✅ Do:
- Use
DefaultSelector; on Linux, BSD and macOS it is the O(k) implementation unregister()a file before closing it, so the lookup stays O(1)- Keep per-connection state in
data, so a ready event needs no second lookup - Close the selector, or use it in a
withblock, to release the kernel object
❌ Avoid:
SelectSelectororPollSelectorfor many mostly idle connections - every call is O(n)SelectSelectoron Unix for descriptors at or aboveFD_SETSIZE;select()raisesValueError
Version Notes¶
- All Python 3: On Windows only sockets can be watched, because
select()there accepts nothing else
Related Modules¶
- select - the primitives each selector wraps
- asyncio - an event loop built on a selector
- socket - the file objects most often registered
- socketserver - servers that wait on a selector between requests