Skip to content
Closed
42 changes: 42 additions & 0 deletions Include/internal/pycore_freelist.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ struct _Py_object_stack_freelist {
Py_ssize_t numfree;
};


struct _Py_asyncmodule_futureiter_freelist {
#ifdef WITH_FREELISTS
struct futureiterobject *fi_freelist;
Py_ssize_t fi_freelist_len;
#endif
};

struct _Py_object_freelists {
struct _Py_float_freelist floats;
struct _Py_tuple_freelist tuples;
Expand All @@ -135,6 +143,7 @@ struct _Py_object_freelists {
struct _Py_async_gen_freelist async_gens;
struct _Py_async_gen_asend_freelist async_gen_asends;
struct _Py_object_stack_freelist object_stacks;
struct _Py_asyncmodule_futureiter_freelist futureiters;
};

extern void _PyObject_ClearFreeLists(struct _Py_object_freelists *freelists, int is_finalization);
Expand All @@ -147,6 +156,39 @@ extern void _PyAsyncGen_ClearFreeLists(struct _Py_object_freelists *freelists, i
extern void _PyContext_ClearFreeList(struct _Py_object_freelists *freelists, int is_finalization);
extern void _PyObjectStackChunk_ClearFreeList(struct _Py_object_freelists *freelists, int is_finalization);

// Keep in sync with _asynciomodule.c !
typedef struct futureiterobject_dummy {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned by Sam we should try to find a way to avoid this.

Copy link
Contributor

@colesbury colesbury Jul 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we go with #121934, then that will make the asyncio freelist implementation a lot easier and will avoid a lot of these warts.

Maybe, in the meantime, we should just disable the freelist in the free-threaded build? That would be a much smaller change and wouldn't introduce duplicate types.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could backport the disabling of freelist in the free-threaded build to 3.13 to fix the race conditions.

PyObject_HEAD
void *future;
} futureiterobject_dummy;

static inline void
_PyAsyncModule_ClearFreeLists(struct _Py_object_freelists *freelists, int is_finalization)
{
#ifdef WITH_FREELISTS
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does anyone with better freelist internals knowledge know why this happens:

When I remove this function's body, the refleak tests pass. Otherwise they currently fail.

PyObject *next;
PyObject *current;

next = (PyObject*) freelists->futureiters.fi_freelist;
while (next != NULL) {
assert(freelists->futureiters.fi_freelist_len > 0);
freelists->futureiters.fi_freelist_len--;

current = next;
next = (PyObject*) ((futureiterobject_dummy*) current)->future;
PyObject_GC_Del(current);
}
assert(freelists->futureiters.fi_freelist_len == 0 || freelists->futureiters.fi_freelist_len == -1);
freelists->futureiters.fi_freelist = NULL;
if (is_finalization) {
freelists->futureiters.fi_freelist_len = -1;
}
else {
freelists->futureiters.fi_freelist_len = 0;
}
#endif
}

#ifdef __cplusplus
}
#endif
Expand Down
7 changes: 7 additions & 0 deletions Lib/test/libregrtest/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,13 @@ def clear_caches():
else:
importlib_metadata.FastPath.__new__.cache_clear()

try:
_asyncio = sys.modules['_asyncio']
except KeyError:
pass
else:
_asyncio._clear_freelist()


def get_build_info():
# Get most important configure and build options as a list of strings.
Expand Down
98 changes: 49 additions & 49 deletions Modules/_asynciomodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,6 @@ typedef struct {
/* Counter for autogenerated Task names */
uint64_t task_name_counter;

futureiterobject *fi_freelist;
Py_ssize_t fi_freelist_len;

/* Linked-list of all tasks which are instances of asyncio.Task or subclasses
of it. Third party tasks implementations which don't inherit from
asyncio.Task are tracked separately using the 'non_asyncio_tasks' WeakSet.
Expand Down Expand Up @@ -217,6 +214,16 @@ get_asyncio_state_by_def(PyObject *self)
#include "clinic/_asynciomodule.c.h"


#ifdef WITH_FREELISTS
static struct _Py_asyncmodule_futureiter_freelist *
get_futureiter_freelist(void)
{
struct _Py_object_freelists *freelists = _Py_object_freelists_GET();
assert(freelists != NULL);
return &freelists->futureiters;
}
#endif

/*[clinic input]
class _asyncio.Future "FutureObj *" "&Future_Type"
[clinic start generated code]*/
Expand Down Expand Up @@ -1579,25 +1586,19 @@ FutureIter_dealloc(futureiterobject *it)

assert(_PyType_HasFeature(tp, Py_TPFLAGS_HEAPTYPE));

PyObject *module = ((PyHeapTypeObject*)tp)->ht_module;
asyncio_state *state = NULL;

PyObject_GC_UnTrack(it);
tp->tp_clear((PyObject *)it);

// GH-115874: We can't use PyType_GetModuleByDef here as the type might have
// already been cleared, which is also why we must check if ht_module != NULL.
if (module && _PyModule_GetDef(module) == &_asynciomodule) {
state = get_asyncio_state(module);
}

// TODO GH-121621: This should be moved to thread state as well.
if (state && state->fi_freelist_len < FI_FREELIST_MAXLEN) {
state->fi_freelist_len++;
it->future = (FutureObj*) state->fi_freelist;
state->fi_freelist = it;
#ifdef WITH_FREELISTS
struct _Py_asyncmodule_futureiter_freelist* freelist = get_futureiter_freelist();
if (freelist->fi_freelist_len < FI_FREELIST_MAXLEN) {
freelist->fi_freelist_len++;
it->future = (FutureObj*) freelist->fi_freelist;
freelist->fi_freelist = it;
}
else {
else
#endif
{
PyObject_GC_Del(it);
Py_DECREF(tp);
}
Expand Down Expand Up @@ -1801,14 +1802,18 @@ future_new_iter(PyObject *fut)
asyncio_state *state = get_asyncio_state_by_def((PyObject *)fut);
ENSURE_FUTURE_ALIVE(state, fut)

if (state->fi_freelist_len) {
state->fi_freelist_len--;
it = state->fi_freelist;
state->fi_freelist = (futureiterobject*) it->future;
#ifdef WITH_FREELISTS
struct _Py_asyncmodule_futureiter_freelist* freelist = get_futureiter_freelist();
if (freelist->fi_freelist_len) {
freelist->fi_freelist_len--;
it = freelist->fi_freelist;
freelist->fi_freelist = (futureiterobject*) it->future;
it->future = NULL;
_Py_NewReference((PyObject*) it);
}
else {
else
#endif
{
it = PyObject_GC_New(futureiterobject, state->FutureIterType);
if (it == NULL) {
return NULL;
Expand Down Expand Up @@ -3264,6 +3269,24 @@ task_wakeup(TaskObj *task, PyObject *o)
/*********************** Functions **************************/


/*[clinic input]
_asyncio._clear_freelist

Clears the asyncio freelist.

Internal CPython implementation detail. Do not depend on this or use it!
This function is thread-specific.

[clinic start generated code]*/

static PyObject *
_asyncio__clear_freelist_impl(PyObject *module)
/*[clinic end generated code: output=8d0e295bbbe2f8b6 input=f3ef7630d66cf63a]*/
{
_PyAsyncModule_ClearFreeLists(_Py_object_freelists_GET(), 0);
Py_RETURN_NONE;
}

/*[clinic input]
_asyncio._get_running_loop

Expand Down Expand Up @@ -3676,24 +3699,6 @@ _asyncio_all_tasks_impl(PyObject *module, PyObject *loop)
return tasks;
}

static void
module_free_freelists(asyncio_state *state)
{
PyObject *next;
PyObject *current;

next = (PyObject*) state->fi_freelist;
while (next != NULL) {
assert(state->fi_freelist_len > 0);
state->fi_freelist_len--;

current = next;
next = (PyObject*) ((futureiterobject*) current)->future;
PyObject_GC_Del(current);
}
assert(state->fi_freelist_len == 0);
state->fi_freelist = NULL;
}

static int
module_traverse(PyObject *mod, visitproc visit, void *arg)
Expand Down Expand Up @@ -3723,13 +3728,6 @@ module_traverse(PyObject *mod, visitproc visit, void *arg)

Py_VISIT(state->context_kwname);

// Visit freelist.
PyObject *next = (PyObject*) state->fi_freelist;
while (next != NULL) {
PyObject *current = next;
Py_VISIT(current);
next = (PyObject*) ((futureiterobject*) current)->future;
}
return 0;
}

Expand Down Expand Up @@ -3761,7 +3759,8 @@ module_clear(PyObject *mod)

Py_CLEAR(state->context_kwname);

module_free_freelists(state);
_PyAsyncModule_ClearFreeLists(_Py_object_freelists_GET(), 0);


return 0;
}
Expand Down Expand Up @@ -3873,6 +3872,7 @@ static PyMethodDef asyncio_methods[] = {
_ASYNCIO__LEAVE_TASK_METHODDEF
_ASYNCIO__SWAP_CURRENT_TASK_METHODDEF
_ASYNCIO_ALL_TASKS_METHODDEF
_ASYNCIO__CLEAR_FREELIST_METHODDEF
{NULL, NULL}
};

Expand Down
23 changes: 22 additions & 1 deletion Modules/clinic/_asynciomodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,8 @@ _PyObject_ClearFreeLists(struct _Py_object_freelists *freelists, int is_finaliza
_PyDict_ClearFreeList(freelists, is_finalization);
_PyContext_ClearFreeList(freelists, is_finalization);
_PyAsyncGen_ClearFreeLists(freelists, is_finalization);
_PyAsyncModule_ClearFreeLists(freelists, is_finalization);

// Only be cleared if is_finalization is true.
_PyObjectStackChunk_ClearFreeList(freelists, is_finalization);
_PySlice_ClearFreeList(freelists, is_finalization);
Expand Down