asyncio/base_events.py
cpython 3.14 @ ab2d84fe1023/Lib/asyncio/base_events.py
Overview
base_events.py (~2000 lines) defines BaseEventLoop, the abstract core
shared by every concrete event loop in the standard library. It owns the
scheduler state (_ready, _scheduled), the I/O polling contract
(_run_once), high-level connection helpers (create_connection,
create_server), and the non-blocking socket coroutines (sock_*). Concrete
loops such as SelectorEventLoop and ProactorEventLoop inherit from this
class and supply only the I/O-specific methods.
Reading
Scheduler state: _ready and _scheduled
BaseEventLoop keeps two separate queues for pending work:
# CPython: Lib/asyncio/base_events.py
self._ready = collections.deque() # Handles due right now
self._scheduled = [] # TimerHandles, kept as a heapq
_ready is a plain deque of Handle objects. Every call_soon appends
here. _scheduled is a min-heap (via heapq) of TimerHandle objects
keyed by their scheduled fire time. call_later and call_at insert into
_scheduled; at the top of each _run_once pass the loop drains all
TimerHandles whose _when <= self.time() into _ready, then executes
every item currently in _ready.
This two-queue design means timers never starve I/O callbacks and I/O
callbacks never starve timers: both are collapsed into _ready before the
batch runs.
_run_once: the single iteration
_run_once is the innermost loop tick. It does three things in order:
# CPython: Lib/asyncio/base_events.py
def _run_once(self):
# 1. Compute the I/O timeout from the nearest scheduled timer.
timeout = None
if self._ready or self._stopping:
timeout = 0
elif self._scheduled:
when = self._scheduled[0]._when
timeout = min(max(0, when - self.time()), MAXIMUM_SELECT_TIMEOUT)
# 2. Poll I/O (implemented by the subclass via self._selector or equivalent).
event_list = self._selector.select(timeout)
self._process_events(event_list)
# 3. Fire all timers that are now due, then run _ready.
end_time = self.time() + self._clock_resolution
while self._scheduled:
handle = self._scheduled[0]
if handle._when >= end_time:
break
handle = heapq.heappop(self._scheduled)
handle._scheduled = False
self._ready.append(handle)
ntodo = len(self._ready)
for _ in range(ntodo):
handle = self._ready.popleft()
...
handle._run()
The timeout passed to select is zero when _ready is non-empty, ensuring
that I/O polling never blocks when there is already work to do. ntodo is
snapshotted before the run loop so that callbacks added during execution are
deferred to the next tick rather than executed in the same batch.
run_until_complete
run_until_complete wraps a coroutine (or future) in a Task, attaches a
done-callback that calls stop(), and then delegates to run_forever:
# CPython: Lib/asyncio/base_events.py
def run_until_complete(self, future):
self._check_closed()
self._check_running()
new_task = not futures.isfuture(future)
future = tasks.ensure_future(future, loop=self)
if new_task:
future._log_destroy_pending = False
future.add_done_callback(_run_until_complete_cb)
try:
self.run_forever()
except:
...
finally:
future.remove_done_callback(_run_until_complete_cb)
...
return future.result()
The done-callback _run_until_complete_cb simply calls loop.stop(), which
sets self._stopping = True. On the next _run_once iteration the loop sees
the flag and breaks out of run_forever's while True body.
gopy mirror
This file is not yet ported. When ported it will live at
module/asyncio/base_events.go. The scheduler queues map naturally to a Go
[]Handle slice (sorted via container/heap) for _scheduled and a
chan Handle or slice for _ready. The _run_once body will wrap a call
to the Go runtime's select-equivalent (likely golang.org/x/sys/unix.Select
or syscall.Select) supplied by the concrete subclass.
CPython 3.14 changes
- The long-deprecated
loopparameter was removed from all high-level asyncio APIs that previously forwarded it toBaseEventLoopmethods.run_until_completeand thesock_*coroutines are unaffected because they live on the loop object itself. create_connectiongained assl_shutdown_timeoutkeyword argument in 3.11; that parameter is present and unchanged in 3.14.- Exception group support (
TaskGroup) was added in 3.11 and refined in 3.12/3.13.BaseEventLoopitself did not change shape for that feature, butrun_until_completenow propagatesExceptionGroupcorrectly through the done-callback path.