Skip to content

Commit f4239df

Browse files
[3.14] gh-140414: add fastpath for current running loop in asyncio.all_tasks (GH-140542) (#144494)
* gh-140414: add fastpath for current running loop in `asyncio.all_tasks` (GH-140542) Optimize `asyncio.all_tasks()` for the common case where the event loop is running in the current thread by avoiding stop-the-world pauses and locking. This optimization is already present for `asyncio.current_task()` so we do the same for `asyncio.all_tasks()`. (cherry picked from commit 95e5d59) Co-authored-by: Kumar Aditya <kumaraditya@python.org>
1 parent 6614a3c commit f4239df

File tree

2 files changed

+40
-24
lines changed

2 files changed

+40
-24
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix performance regression in :func:`asyncio.all_tasks` on
2+
:term:`free-threaded builds <free-threaded build>`. Patch by Kumar Aditya.

Modules/_asynciomodule.c

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4075,30 +4075,44 @@ _asyncio_all_tasks_impl(PyObject *module, PyObject *loop)
40754075
return NULL;
40764076
}
40774077

4078-
PyInterpreterState *interp = PyInterpreterState_Get();
4079-
// Stop the world and traverse the per-thread linked list
4080-
// of asyncio tasks for every thread, as well as the
4081-
// interpreter's linked list, and add them to `tasks`.
4082-
// The interpreter linked list is used for any lingering tasks
4083-
// whose thread state has been deallocated while the task was
4084-
// still alive. This can happen if a task is referenced by
4085-
// a different thread, in which case the task is moved to
4086-
// the interpreter's linked list from the thread's linked
4087-
// list before deallocation. See PyThreadState_Clear.
4088-
//
4089-
// The stop-the-world pause is required so that no thread
4090-
// modifies its linked list while being iterated here
4091-
// in parallel. This design allows for lock-free
4092-
// register_task/unregister_task for loops running in parallel
4093-
// in different threads (the general case).
4094-
_PyEval_StopTheWorld(interp);
4095-
int ret = add_tasks_interp(interp, (PyListObject *)tasks);
4096-
_PyEval_StartTheWorld(interp);
4097-
if (ret < 0) {
4098-
// call any escaping calls after starting the world to avoid any deadlocks.
4099-
Py_DECREF(tasks);
4100-
Py_DECREF(loop);
4101-
return NULL;
4078+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)_PyThreadState_GET();
4079+
if (ts->asyncio_running_loop == loop) {
4080+
// Fast path for the current running loop of current thread
4081+
// no locking or stop the world pause is required
4082+
struct llist_node *head = &ts->asyncio_tasks_head;
4083+
if (add_tasks_llist(head, (PyListObject *)tasks) < 0) {
4084+
Py_DECREF(tasks);
4085+
Py_DECREF(loop);
4086+
return NULL;
4087+
}
4088+
}
4089+
else {
4090+
// Slow path for loop running in different thread
4091+
PyInterpreterState *interp = ts->base.interp;
4092+
// Stop the world and traverse the per-thread linked list
4093+
// of asyncio tasks for every thread, as well as the
4094+
// interpreter's linked list, and add them to `tasks`.
4095+
// The interpreter linked list is used for any lingering tasks
4096+
// whose thread state has been deallocated while the task was
4097+
// still alive. This can happen if a task is referenced by
4098+
// a different thread, in which case the task is moved to
4099+
// the interpreter's linked list from the thread's linked
4100+
// list before deallocation. See PyThreadState_Clear.
4101+
//
4102+
// The stop-the-world pause is required so that no thread
4103+
// modifies its linked list while being iterated here
4104+
// in parallel. This design allows for lock-free
4105+
// register_task/unregister_task for loops running in parallel
4106+
// in different threads (the general case).
4107+
_PyEval_StopTheWorld(interp);
4108+
int ret = add_tasks_interp(interp, (PyListObject *)tasks);
4109+
_PyEval_StartTheWorld(interp);
4110+
if (ret < 0) {
4111+
// call any escaping calls after starting the world to avoid any deadlocks.
4112+
Py_DECREF(tasks);
4113+
Py_DECREF(loop);
4114+
return NULL;
4115+
}
41024116
}
41034117

41044118
// All the tasks are now in the list, now filter the tasks which are done

0 commit comments

Comments
 (0)