Skip to main content

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 loop parameter was removed from all high-level asyncio APIs that previously forwarded it to BaseEventLoop methods. run_until_complete and the sock_* coroutines are unaffected because they live on the loop object itself.
  • create_connection gained a ssl_shutdown_timeout keyword 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. BaseEventLoop itself did not change shape for that feature, but run_until_complete now propagates ExceptionGroup correctly through the done-callback path.