Skip to main content

Objects/genobject.c (part 5)

Source:

cpython 3.14 @ ab2d84fe1023/Objects/genobject.c

This annotation covers async generators. See objects_generatorobject4_detail for sync generators, coroutines, send, throw, and close.

Map

LinesSymbolRole
1-80AsyncGenerator type objectFields beyond Generator: ag_running, ag_origin
81-180asend objectAwaitable produced by ag.asend(value)
181-280athrow objectAwaitable produced by ag.athrow(exc)
281-380acloseFinalization: throw GeneratorExit asynchronously
381-500ag_finalizer / sys.set_asyncgen_finalizerHook for event loop cleanup

Reading

asend object

// CPython: Objects/genobject.c:680 async_gen_asend_iternext
static PyObject *
async_gen_asend_iternext(PyAsyncGenASend *o)
{
/* Drives one step of the async generator */
PyAsyncGenObject *gen = o->ags_gen;
if (o->ags_state == AWAITABLE_STATE_CLOSED)
return NULL;
PyObject *result = gen_send_ex2(gen->ag_gen, o->ags_sendval, 0, 0);
if (result && _PyAsyncGenValueWrapper_CheckExact(result)) {
/* Got a yielded value: unwrap and return for await */
PyObject *val = ((PyAsyncGenValueWrapper *)result)->agvw_val;
Py_DECREF(result);
o->ags_state = AWAITABLE_STATE_ITER;
return val;
}
o->ags_state = AWAITABLE_STATE_CLOSED;
return result;
}

ag.asend(val) returns an async_gen_asend object. Each __next__ call on it advances the async generator by one yield. The yielded value is wrapped in PyAsyncGenValueWrapper to distinguish it from a return value.

ag_running guard

// CPython: Objects/genobject.c:620 async_gen_send
static PyObject *
async_gen_send_impl(PyAsyncGenObject *o, PyObject *arg)
{
if (o->ag_running) {
PyErr_SetString(PyExc_ValueError,
"asynchronous generator already running");
return NULL;
}
o->ag_running = 1;
PyObject *result = gen_send_ex2(o->ag_gen, arg, 0, 0);
o->ag_running = 0;
return result;
}

ag_running prevents re-entering an async generator while it is executing. Unlike sync generators (which get a ValueError from the frame's f_executing flag), async generators need an explicit guard because the event loop can interleave coroutine steps.

ag_finalizer

// CPython: Objects/genobject.c:820 async_gen_dealloc
static void
async_gen_dealloc(PyAsyncGenObject *gen)
{
/* If the generator was not exhausted, call ag_finalizer */
if (gen->ag_origin_or_finalizer != NULL &&
gen->ag_closed == 0) {
/* Schedule aclose() on the event loop */
PyObject *finalizer = gen->ag_origin_or_finalizer;
PyObject_CallOneArg(finalizer, (PyObject *)gen);
}
gen_dealloc((PyGenObject *)gen);
}

sys.set_asyncgen_finalizer(hook) registers a hook called when an async generator is garbage-collected before exhaustion. asyncio uses this to schedule aclose() on the event loop, ensuring finally blocks run. Without this hook, unfinished async generators silently leak resources.

aclose

// CPython: Objects/genobject.c:760 async_gen_aclose
static PyObject *
async_gen_aclose(PyAsyncGenObject *o, PyObject *Py_UNUSED(arg))
{
if (o->ag_closed) Py_RETURN_NONE;
return async_gen_athrow_new(o, NULL);
}

await ag.aclose() creates an athrow with None (which maps to throwing GeneratorExit). The async generator's finally block runs asynchronously. After aclose, ag_closed is set and further asend/asend raise StopAsyncIteration.

gopy notes

AsyncGenerator is objects.AsyncGenerator in objects/instance.go. asend and athrow are awaitable wrappers in objects/namespace.go. ag_running is a bool field on the Go struct. ag_finalizer is registered via module/sys.SetAsyncgenFinalizer and called from the goroutine finalizer.