Skip to main content

Modules/_asynciomodule.c

cpython 3.14 @ ab2d84fe1023/Modules/_asynciomodule.c

_asynciomodule.c is the C accelerator that backs asyncio.Future and asyncio.Task. The pure-Python implementations in asyncio/futures.py and asyncio/tasks.py delegate to these types when the C extension is available, which is the default on CPython. The C types are structurally identical to their Python counterparts; they exist purely for throughput.

The file defines four allocated types: FutureObj, TaskObj, futureiterobject (the iterator returned by Future.__await__), and TaskStepMethWrapper (an internal callable that binds a task to a single step invocation). All four share the same per-module state struct asyncio_state, which caches references to imported helpers such as asyncio.CancelledError and asyncio.InvalidStateError.

TaskObj extends FutureObj by embedding the same FutureObj_HEAD macro, so the two types are layout-compatible at their common prefix. External debuggers (e.g., py-spy) rely on the published _AsyncioDebug section that exposes field offsets into these structs.

Map

LinesSymbolRolegopy
1-100FutureObj_HEAD, FutureObj, TaskObj, TaskStepMethWrapper, asyncio_stateCore struct definitions and per-module state.module/asyncio/
100-400_AsyncioDebug, get_asyncio_state, get_asyncio_state_by_cls, get_asyncio_state_by_defDebug offset section and state-accessor helpers used by every method in the file.module/asyncio/
400-900future_is_alive, future_ensure_alive, future_schedule_callbacks, future_init, future_set_result, future_set_exception, future_cancel, future_add_done_callbackInternal Future helpers: lifecycle guards, callback dispatch, state transitions.module/asyncio/
900-1800_asyncio_Future___init__, FutureObj_clear, FutureObj_traverse, _asyncio_Future_result_impl, _asyncio_Future_exception_impl, _asyncio_Future_add_done_callback_impl, _asyncio_Future_cancel_impl, FutureType_methods, Future_spec, FutureObj_deallocAll exported Future methods, type slot table, and dealloc.module/asyncio/
1800-2300futureiterobject, FutureIter_am_send_lock_held, future_new_iter, FutureIter_specFuture.__await__ iterator: the coroutine protocol bridge.module/asyncio/
2243-2410enter_task, leave_task, swap_current_taskRunning-task tracking on _PyThreadStateImpl.module/asyncio/
2310-2700_asyncio_Task___init___impl, TaskObj_clear, TaskObj_traverse, task_call_step_soon, task_eager_startTask construction, GC slots, eager-start fast path.module/asyncio/
3070-3475task_step_impl, task_step_handle_result_impl, task_step, task_eager_start, task_wakeup_lock_heldThe coroutine step loop. The heart of asyncio scheduling.module/asyncio/
3475-4429_asyncio__enter_task_impl, _asyncio__leave_task_impl, asyncio_methods, module_exec, _asynciomoduleModule-level helpers, method table, and PyInit__asyncio.module/asyncio/

Reading

FutureObj layout (lines 25 to 100)

cpython 3.14 @ ab2d84fe1023/Modules/_asynciomodule.c#L25-100

FutureObj_HEAD is a macro that stamps the same field set into both FutureObj and TaskObj. The two structs are layout-compatible at their common prefix, which lets helper functions accept FutureObj * and work correctly on a TaskObj * via casting:

#define FutureObj_HEAD(prefix) \
PyObject_HEAD \
PyObject *prefix##_loop; \
PyObject *prefix##_callback0; \
PyObject *prefix##_context0; \
PyObject *prefix##_callbacks; \
PyObject *prefix##_exception; \
PyObject *prefix##_exception_tb; \
PyObject *prefix##_result; \
PyObject *prefix##_source_tb; \
PyObject *prefix##_cancel_msg; \
PyObject *prefix##_cancelled_exc; \
PyObject *prefix##_awaited_by; \
fut_state prefix##_state; \
char prefix##_is_task; \
char prefix##_awaited_by_is_set; \
unsigned prefix##_log_tb: 1; \
unsigned prefix##_blocking: 1;

The callback0 / context0 pair is a fast-path inline slot for the common case where a Future has exactly one done callback. When a second callback is added, the first is moved into fut_callbacks, a list of (callable, context) tuples. This avoids a heap allocation for the majority of Futures that are awaited by exactly one coroutine.

TaskObj adds the coroutine pointer (task_coro), a waiter reference (task_fut_waiter), a cancel request counter, and an llist_node so that all live tasks are linked into a per-interpreter list for asyncio.all_tasks.

Task.step execution (lines 3070 to 3475)

cpython 3.14 @ ab2d84fe1023/Modules/_asynciomodule.c#L3070-3475

task_step is the public entry point. It calls enter_task, delegates to task_step_impl, then calls leave_task:

static PyObject *
task_step(asyncio_state *state, TaskObj *task, PyObject *exc)
{
PyObject *res;

if (enter_task(task->task_loop, (PyObject*)task) < 0) {
return NULL;
}

res = task_step_impl(state, task, exc);

if (res == NULL) {
PyObject *exc = PyErr_GetRaisedException();
leave_task(task->task_loop, (PyObject*)task);
_PyErr_ChainExceptions1(exc);
return NULL;
}
else {
if (leave_task(task->task_loop, (PyObject*)task) < 0) {
Py_DECREF(res);
return NULL;
}
else {
return res;
}
}
}

task_step_impl drives the coroutine one step. When no exception is pending it sends None via PyIter_Send. When the coroutine raises StopIteration (i.e., it returned), the task resolves to the stop value. Any other raised exception is forwarded to future_set_exception. A PYGEN_YIELD result lands in task_step_handle_result_impl, which inspects the yielded object: if it is another Future, the task registers itself as a done callback on that Future so it will be woken up when the dependency resolves:

int gen_status = PYGEN_ERROR;
if (exc == NULL) {
gen_status = PyIter_Send(coro, Py_None, &result);
}
else {
result = PyObject_CallMethodOneArg(coro, &_Py_ID(throw), exc);
gen_status = gen_status_from_result(&result);
}

enter_task / leave_task (lines 2243 to 2310)

cpython 3.14 @ ab2d84fe1023/Modules/_asynciomodule.c#L2243-2310

These two functions maintain the asyncio_running_task slot on _PyThreadStateImpl. The slot is checked to prevent re-entering a task while another is executing and to associate the running task with the running loop:

static int
enter_task(PyObject *loop, PyObject *task)
{
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)_PyThreadState_GET();

if (ts->asyncio_running_loop != loop) {
PyErr_Format(PyExc_RuntimeError,
"loop %R is not the running loop", loop);
return -1;
}
if (ts->asyncio_running_task != NULL) {
PyErr_Format(
PyExc_RuntimeError,
"Cannot enter into task %R while another "
"task %R is being executed.",
task, ts->asyncio_running_task);
return -1;
}
ts->asyncio_running_task = Py_NewRef(task);
return 0;
}

leave_task mirrors this: it validates that the task being left is the one currently marked as running, then clears the slot with Py_CLEAR.

gopy mirror

module/asyncio/ (pending). The Go port represents FutureObj and TaskObj as Go structs with the same field names. The callback fast-path (callback0 / context0) carries over directly as inline fields. The enter_task / leave_task invariant is enforced with a per-goroutine context value rather than a thread-state slot. The coroutine step loop maps onto Go channels: task_step_impl becomes a function called from the event loop goroutine, and task_wakeup posts back to that goroutine via a channel send.