gh-128002: use per threads tasks linked list in asyncio (#128869)
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
This commit is contained in:
parent
b4ff8b22b3
commit
0d68b14a0d
@ -227,6 +227,13 @@ struct _is {
|
|||||||
PyMutex weakref_locks[NUM_WEAKREF_LIST_LOCKS];
|
PyMutex weakref_locks[NUM_WEAKREF_LIST_LOCKS];
|
||||||
_PyIndexPool tlbc_indices;
|
_PyIndexPool tlbc_indices;
|
||||||
#endif
|
#endif
|
||||||
|
// Per-interpreter list of tasks, any lingering tasks from thread
|
||||||
|
// states gets added here and removed from the corresponding
|
||||||
|
// thread state's list.
|
||||||
|
struct llist_node asyncio_tasks_head;
|
||||||
|
// `asyncio_tasks_lock` is used when tasks are moved
|
||||||
|
// from thread's list to interpreter's list.
|
||||||
|
PyMutex asyncio_tasks_lock;
|
||||||
|
|
||||||
// Per-interpreter state for the obmalloc allocator. For the main
|
// Per-interpreter state for the obmalloc allocator. For the main
|
||||||
// interpreter and for all interpreters that don't have their
|
// interpreter and for all interpreters that don't have their
|
||||||
|
@ -52,7 +52,7 @@ typedef enum _PyLockFlags {
|
|||||||
|
|
||||||
// Lock a mutex with an optional timeout and additional options. See
|
// Lock a mutex with an optional timeout and additional options. See
|
||||||
// _PyLockFlags for details.
|
// _PyLockFlags for details.
|
||||||
extern PyLockStatus
|
extern PyAPI_FUNC(PyLockStatus)
|
||||||
_PyMutex_LockTimed(PyMutex *m, PyTime_t timeout_ns, _PyLockFlags flags);
|
_PyMutex_LockTimed(PyMutex *m, PyTime_t timeout_ns, _PyLockFlags flags);
|
||||||
|
|
||||||
// Lock a mutex with additional options. See _PyLockFlags for details.
|
// Lock a mutex with additional options. See _PyLockFlags for details.
|
||||||
|
@ -182,8 +182,8 @@ extern void _PyEval_StartTheWorldAll(_PyRuntimeState *runtime);
|
|||||||
// Perform a stop-the-world pause for threads in the specified interpreter.
|
// Perform a stop-the-world pause for threads in the specified interpreter.
|
||||||
//
|
//
|
||||||
// NOTE: This is a no-op outside of Py_GIL_DISABLED builds.
|
// NOTE: This is a no-op outside of Py_GIL_DISABLED builds.
|
||||||
extern void _PyEval_StopTheWorld(PyInterpreterState *interp);
|
extern PyAPI_FUNC(void) _PyEval_StopTheWorld(PyInterpreterState *interp);
|
||||||
extern void _PyEval_StartTheWorld(PyInterpreterState *interp);
|
extern PyAPI_FUNC(void) _PyEval_StartTheWorld(PyInterpreterState *interp);
|
||||||
|
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
|
@ -24,9 +24,14 @@ typedef struct _PyThreadStateImpl {
|
|||||||
PyObject *asyncio_running_loop; // Strong reference
|
PyObject *asyncio_running_loop; // Strong reference
|
||||||
PyObject *asyncio_running_task; // Strong reference
|
PyObject *asyncio_running_task; // Strong reference
|
||||||
|
|
||||||
|
/* Head of circular linked-list of all tasks which are instances of `asyncio.Task`
|
||||||
|
or subclasses of it used in `asyncio.all_tasks`.
|
||||||
|
*/
|
||||||
|
struct llist_node asyncio_tasks_head;
|
||||||
struct _qsbr_thread_state *qsbr; // only used by free-threaded build
|
struct _qsbr_thread_state *qsbr; // only used by free-threaded build
|
||||||
struct llist_node mem_free_queue; // delayed free queue
|
struct llist_node mem_free_queue; // delayed free queue
|
||||||
|
|
||||||
|
|
||||||
#ifdef Py_GIL_DISABLED
|
#ifdef Py_GIL_DISABLED
|
||||||
struct _gc_thread_state gc;
|
struct _gc_thread_state gc;
|
||||||
struct _mimalloc_thread_state mimalloc;
|
struct _mimalloc_thread_state mimalloc;
|
||||||
|
@ -3,7 +3,8 @@ import threading
|
|||||||
import unittest
|
import unittest
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
import weakref
|
||||||
|
from test import support
|
||||||
from test.support import threading_helper
|
from test.support import threading_helper
|
||||||
|
|
||||||
threading_helper.requires_working_threading(module=True)
|
threading_helper.requires_working_threading(module=True)
|
||||||
@ -95,6 +96,22 @@ class TestFreeThreading:
|
|||||||
done.set()
|
done.set()
|
||||||
runner.join()
|
runner.join()
|
||||||
|
|
||||||
|
def test_task_different_thread_finalized(self) -> None:
|
||||||
|
task = None
|
||||||
|
async def func():
|
||||||
|
nonlocal task
|
||||||
|
task = asyncio.current_task()
|
||||||
|
|
||||||
|
thread = Thread(target=lambda: asyncio.run(func()))
|
||||||
|
thread.start()
|
||||||
|
thread.join()
|
||||||
|
wr = weakref.ref(task)
|
||||||
|
del thread
|
||||||
|
del task
|
||||||
|
# task finalization in different thread shouldn't crash
|
||||||
|
support.gc_collect()
|
||||||
|
self.assertIsNone(wr())
|
||||||
|
|
||||||
def test_run_coroutine_threadsafe(self) -> None:
|
def test_run_coroutine_threadsafe(self) -> None:
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
|
@ -67,6 +67,10 @@ typedef struct TaskObj {
|
|||||||
PyObject *task_name;
|
PyObject *task_name;
|
||||||
PyObject *task_context;
|
PyObject *task_context;
|
||||||
struct llist_node task_node;
|
struct llist_node task_node;
|
||||||
|
#ifdef Py_GIL_DISABLED
|
||||||
|
// thread id of the thread where this task was created
|
||||||
|
uintptr_t task_tid;
|
||||||
|
#endif
|
||||||
} TaskObj;
|
} TaskObj;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@ -94,14 +98,6 @@ typedef struct {
|
|||||||
|| PyObject_TypeCheck(obj, state->FutureType) \
|
|| PyObject_TypeCheck(obj, state->FutureType) \
|
||||||
|| PyObject_TypeCheck(obj, state->TaskType))
|
|| PyObject_TypeCheck(obj, state->TaskType))
|
||||||
|
|
||||||
#ifdef Py_GIL_DISABLED
|
|
||||||
# define ASYNCIO_STATE_LOCK(state) Py_BEGIN_CRITICAL_SECTION_MUT(&state->mutex)
|
|
||||||
# define ASYNCIO_STATE_UNLOCK(state) Py_END_CRITICAL_SECTION()
|
|
||||||
#else
|
|
||||||
# define ASYNCIO_STATE_LOCK(state) ((void)state)
|
|
||||||
# define ASYNCIO_STATE_UNLOCK(state) ((void)state)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
typedef struct _Py_AsyncioModuleDebugOffsets {
|
typedef struct _Py_AsyncioModuleDebugOffsets {
|
||||||
struct _asyncio_task_object {
|
struct _asyncio_task_object {
|
||||||
uint64_t size;
|
uint64_t size;
|
||||||
@ -135,9 +131,6 @@ GENERATE_DEBUG_SECTION(AsyncioDebug, Py_AsyncioModuleDebugOffsets AsyncioDebug)
|
|||||||
|
|
||||||
/* State of the _asyncio module */
|
/* State of the _asyncio module */
|
||||||
typedef struct {
|
typedef struct {
|
||||||
#ifdef Py_GIL_DISABLED
|
|
||||||
PyMutex mutex;
|
|
||||||
#endif
|
|
||||||
PyTypeObject *FutureIterType;
|
PyTypeObject *FutureIterType;
|
||||||
PyTypeObject *TaskStepMethWrapper_Type;
|
PyTypeObject *TaskStepMethWrapper_Type;
|
||||||
PyTypeObject *FutureType;
|
PyTypeObject *FutureType;
|
||||||
@ -184,11 +177,6 @@ typedef struct {
|
|||||||
/* Counter for autogenerated Task names */
|
/* Counter for autogenerated Task names */
|
||||||
uint64_t task_name_counter;
|
uint64_t task_name_counter;
|
||||||
|
|
||||||
/* Head of circular 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.
|
|
||||||
*/
|
|
||||||
struct llist_node asyncio_tasks_head;
|
|
||||||
} asyncio_state;
|
} asyncio_state;
|
||||||
|
|
||||||
static inline asyncio_state *
|
static inline asyncio_state *
|
||||||
@ -2179,16 +2167,15 @@ static PyMethodDef TaskWakeupDef = {
|
|||||||
static void
|
static void
|
||||||
register_task(asyncio_state *state, TaskObj *task)
|
register_task(asyncio_state *state, TaskObj *task)
|
||||||
{
|
{
|
||||||
ASYNCIO_STATE_LOCK(state);
|
|
||||||
assert(Task_Check(state, task));
|
assert(Task_Check(state, task));
|
||||||
if (task->task_node.next != NULL) {
|
if (task->task_node.next != NULL) {
|
||||||
// already registered
|
// already registered
|
||||||
assert(task->task_node.prev != NULL);
|
assert(task->task_node.prev != NULL);
|
||||||
goto exit;
|
return;
|
||||||
}
|
}
|
||||||
llist_insert_tail(&state->asyncio_tasks_head, &task->task_node);
|
_PyThreadStateImpl *tstate = (_PyThreadStateImpl *) _PyThreadState_GET();
|
||||||
exit:
|
struct llist_node *head = &tstate->asyncio_tasks_head;
|
||||||
ASYNCIO_STATE_UNLOCK(state);
|
llist_insert_tail(head, &task->task_node);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
@ -2197,19 +2184,38 @@ register_eager_task(asyncio_state *state, PyObject *task)
|
|||||||
return PySet_Add(state->eager_tasks, task);
|
return PySet_Add(state->eager_tasks, task);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static inline void
|
||||||
unregister_task(asyncio_state *state, TaskObj *task)
|
unregister_task_safe(TaskObj *task)
|
||||||
{
|
{
|
||||||
ASYNCIO_STATE_LOCK(state);
|
|
||||||
assert(Task_Check(state, task));
|
|
||||||
if (task->task_node.next == NULL) {
|
if (task->task_node.next == NULL) {
|
||||||
// not registered
|
// not registered
|
||||||
assert(task->task_node.prev == NULL);
|
assert(task->task_node.prev == NULL);
|
||||||
goto exit;
|
return;
|
||||||
}
|
}
|
||||||
llist_remove(&task->task_node);
|
llist_remove(&task->task_node);
|
||||||
exit:
|
}
|
||||||
ASYNCIO_STATE_UNLOCK(state);
|
|
||||||
|
static void
|
||||||
|
unregister_task(asyncio_state *state, TaskObj *task)
|
||||||
|
{
|
||||||
|
assert(Task_Check(state, task));
|
||||||
|
#ifdef Py_GIL_DISABLED
|
||||||
|
// check if we are in the same thread
|
||||||
|
// if so, we can avoid locking
|
||||||
|
if (task->task_tid == _Py_ThreadId()) {
|
||||||
|
unregister_task_safe(task);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// we are in a different thread
|
||||||
|
// stop the world then check and remove the task
|
||||||
|
PyThreadState *tstate = _PyThreadState_GET();
|
||||||
|
_PyEval_StopTheWorld(tstate->interp);
|
||||||
|
unregister_task_safe(task);
|
||||||
|
_PyEval_StartTheWorld(tstate->interp);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
unregister_task_safe(task);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
@ -2423,6 +2429,9 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
|
|||||||
}
|
}
|
||||||
|
|
||||||
Py_CLEAR(self->task_fut_waiter);
|
Py_CLEAR(self->task_fut_waiter);
|
||||||
|
#ifdef Py_GIL_DISABLED
|
||||||
|
self->task_tid = _Py_ThreadId();
|
||||||
|
#endif
|
||||||
self->task_must_cancel = 0;
|
self->task_must_cancel = 0;
|
||||||
self->task_log_destroy_pending = 1;
|
self->task_log_destroy_pending = 1;
|
||||||
self->task_num_cancels_requested = 0;
|
self->task_num_cancels_requested = 0;
|
||||||
@ -3981,6 +3990,7 @@ _asyncio_current_task_impl(PyObject *module, PyObject *loop)
|
|||||||
static inline int
|
static inline int
|
||||||
add_one_task(asyncio_state *state, PyObject *tasks, PyObject *task, PyObject *loop)
|
add_one_task(asyncio_state *state, PyObject *tasks, PyObject *task, PyObject *loop)
|
||||||
{
|
{
|
||||||
|
assert(PySet_CheckExact(tasks));
|
||||||
PyObject *done = PyObject_CallMethodNoArgs(task, &_Py_ID(done));
|
PyObject *done = PyObject_CallMethodNoArgs(task, &_Py_ID(done));
|
||||||
if (done == NULL) {
|
if (done == NULL) {
|
||||||
return -1;
|
return -1;
|
||||||
@ -4003,6 +4013,57 @@ add_one_task(asyncio_state *state, PyObject *tasks, PyObject *task, PyObject *lo
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline int
|
||||||
|
add_tasks_llist(struct llist_node *head, PyListObject *tasks)
|
||||||
|
{
|
||||||
|
struct llist_node *node;
|
||||||
|
llist_for_each_safe(node, head) {
|
||||||
|
TaskObj *task = llist_data(node, TaskObj, task_node);
|
||||||
|
// The linked list holds borrowed references to task
|
||||||
|
// as such it is possible that the task is concurrently
|
||||||
|
// deallocated while added to this list.
|
||||||
|
// To protect against concurrent deallocations,
|
||||||
|
// we first try to incref the task which would fail
|
||||||
|
// if it is concurrently getting deallocated in another thread,
|
||||||
|
// otherwise it gets added to the list.
|
||||||
|
if (_Py_TryIncref((PyObject *)task)) {
|
||||||
|
if (_PyList_AppendTakeRef(tasks, (PyObject *)task) < 0) {
|
||||||
|
// do not call any escaping calls here while the world is stopped.
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int
|
||||||
|
add_tasks_interp(PyInterpreterState *interp, PyListObject *tasks)
|
||||||
|
{
|
||||||
|
#ifdef Py_GIL_DISABLED
|
||||||
|
assert(interp->stoptheworld.world_stopped);
|
||||||
|
#endif
|
||||||
|
// Start traversing from interpreter's linked list
|
||||||
|
struct llist_node *head = &interp->asyncio_tasks_head;
|
||||||
|
|
||||||
|
if (add_tasks_llist(head, tasks) < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = 0;
|
||||||
|
// traverse the task lists of thread states
|
||||||
|
_Py_FOR_EACH_TSTATE_BEGIN(interp, p) {
|
||||||
|
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)p;
|
||||||
|
head = &ts->asyncio_tasks_head;
|
||||||
|
if (add_tasks_llist(head, tasks) < 0) {
|
||||||
|
ret = -1;
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exit:
|
||||||
|
_Py_FOR_EACH_TSTATE_END(interp);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
/*********************** Module **************************/
|
/*********************** Module **************************/
|
||||||
|
|
||||||
/*[clinic input]
|
/*[clinic input]
|
||||||
@ -4041,30 +4102,29 @@ _asyncio_all_tasks_impl(PyObject *module, PyObject *loop)
|
|||||||
Py_DECREF(loop);
|
Py_DECREF(loop);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
int err = 0;
|
PyInterpreterState *interp = PyInterpreterState_Get();
|
||||||
ASYNCIO_STATE_LOCK(state);
|
// Stop the world and traverse the per-thread linked list
|
||||||
struct llist_node *node;
|
// of asyncio tasks for every thread, as well as the
|
||||||
|
// interpreter's linked list, and add them to `tasks`.
|
||||||
llist_for_each_safe(node, &state->asyncio_tasks_head) {
|
// The interpreter linked list is used for any lingering tasks
|
||||||
TaskObj *task = llist_data(node, TaskObj, task_node);
|
// whose thread state has been deallocated while the task was
|
||||||
// The linked list holds borrowed references to task
|
// still alive. This can happen if a task is referenced by
|
||||||
// as such it is possible that the task is concurrently
|
// a different thread, in which case the task is moved to
|
||||||
// deallocated while added to this list.
|
// the interpreter's linked list from the thread's linked
|
||||||
// To protect against concurrent deallocations,
|
// list before deallocation. See PyThreadState_Clear.
|
||||||
// we first try to incref the task which would fail
|
//
|
||||||
// if it is concurrently getting deallocated in another thread,
|
// The stop-the-world pause is required so that no thread
|
||||||
// otherwise it gets added to the list.
|
// modifies its linked list while being iterated here
|
||||||
if (_Py_TryIncref((PyObject *)task)) {
|
// in parallel. This design allows for lock-free
|
||||||
if (_PyList_AppendTakeRef((PyListObject *)tasks, (PyObject *)task) < 0) {
|
// register_task/unregister_task for loops running in parallel
|
||||||
Py_DECREF(tasks);
|
// in different threads (the general case).
|
||||||
Py_DECREF(loop);
|
_PyEval_StopTheWorld(interp);
|
||||||
err = 1;
|
int ret = add_tasks_interp(interp, (PyListObject *)tasks);
|
||||||
break;
|
_PyEval_StartTheWorld(interp);
|
||||||
}
|
if (ret < 0) {
|
||||||
}
|
// call any escaping calls after starting the world to avoid any deadlocks.
|
||||||
}
|
Py_DECREF(tasks);
|
||||||
ASYNCIO_STATE_UNLOCK(state);
|
Py_DECREF(loop);
|
||||||
if (err) {
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
PyObject *scheduled_iter = PyObject_GetIter(state->non_asyncio_tasks);
|
PyObject *scheduled_iter = PyObject_GetIter(state->non_asyncio_tasks);
|
||||||
@ -4348,7 +4408,6 @@ module_exec(PyObject *mod)
|
|||||||
{
|
{
|
||||||
asyncio_state *state = get_asyncio_state(mod);
|
asyncio_state *state = get_asyncio_state(mod);
|
||||||
|
|
||||||
llist_init(&state->asyncio_tasks_head);
|
|
||||||
|
|
||||||
#define CREATE_TYPE(m, tp, spec, base) \
|
#define CREATE_TYPE(m, tp, spec, base) \
|
||||||
do { \
|
do { \
|
||||||
|
@ -643,6 +643,8 @@ init_interpreter(PyInterpreterState *interp,
|
|||||||
_Py_brc_init_state(interp);
|
_Py_brc_init_state(interp);
|
||||||
#endif
|
#endif
|
||||||
llist_init(&interp->mem_free_queue.head);
|
llist_init(&interp->mem_free_queue.head);
|
||||||
|
llist_init(&interp->asyncio_tasks_head);
|
||||||
|
interp->asyncio_tasks_lock = (PyMutex){0};
|
||||||
for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
|
for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) {
|
||||||
interp->monitors.tools[i] = 0;
|
interp->monitors.tools[i] = 0;
|
||||||
}
|
}
|
||||||
@ -1512,7 +1514,7 @@ init_threadstate(_PyThreadStateImpl *_tstate,
|
|||||||
tstate->delete_later = NULL;
|
tstate->delete_later = NULL;
|
||||||
|
|
||||||
llist_init(&_tstate->mem_free_queue);
|
llist_init(&_tstate->mem_free_queue);
|
||||||
|
llist_init(&_tstate->asyncio_tasks_head);
|
||||||
if (interp->stoptheworld.requested || _PyRuntime.stoptheworld.requested) {
|
if (interp->stoptheworld.requested || _PyRuntime.stoptheworld.requested) {
|
||||||
// Start in the suspended state if there is an ongoing stop-the-world.
|
// Start in the suspended state if there is an ongoing stop-the-world.
|
||||||
tstate->state = _Py_THREAD_SUSPENDED;
|
tstate->state = _Py_THREAD_SUSPENDED;
|
||||||
@ -1692,6 +1694,14 @@ PyThreadState_Clear(PyThreadState *tstate)
|
|||||||
Py_CLEAR(((_PyThreadStateImpl *)tstate)->asyncio_running_loop);
|
Py_CLEAR(((_PyThreadStateImpl *)tstate)->asyncio_running_loop);
|
||||||
Py_CLEAR(((_PyThreadStateImpl *)tstate)->asyncio_running_task);
|
Py_CLEAR(((_PyThreadStateImpl *)tstate)->asyncio_running_task);
|
||||||
|
|
||||||
|
|
||||||
|
PyMutex_Lock(&tstate->interp->asyncio_tasks_lock);
|
||||||
|
// merge any lingering tasks from thread state to interpreter's
|
||||||
|
// tasks list
|
||||||
|
llist_concat(&tstate->interp->asyncio_tasks_head,
|
||||||
|
&((_PyThreadStateImpl *)tstate)->asyncio_tasks_head);
|
||||||
|
PyMutex_Unlock(&tstate->interp->asyncio_tasks_lock);
|
||||||
|
|
||||||
Py_CLEAR(tstate->dict);
|
Py_CLEAR(tstate->dict);
|
||||||
Py_CLEAR(tstate->async_exc);
|
Py_CLEAR(tstate->async_exc);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user