Skip to content

Commit fc3d400

Browse files
ambvkumaraditya303
andauthored
gh-91048: Also clear and set ts->asyncio_running_task with eager tasks (#129197)
This was missing from gh-124640. It's already covered by the new test_asyncio/test_free_threading.py in combination with the runtime assertion in set_ts_asyncio_running_task. Co-authored-by: Kumar Aditya <[email protected]>
1 parent dbb25ce commit fc3d400

File tree

1 file changed

+59
-49
lines changed

1 file changed

+59
-49
lines changed

Modules/_asynciomodule.c

+59-49
Original file line numberDiff line numberDiff line change
@@ -2063,6 +2063,8 @@ static int task_call_step_soon(asyncio_state *state, TaskObj *, PyObject *);
20632063
static PyObject * task_wakeup(TaskObj *, PyObject *);
20642064
static PyObject * task_step(asyncio_state *, TaskObj *, PyObject *);
20652065
static int task_eager_start(asyncio_state *state, TaskObj *task);
2066+
static inline void clear_ts_asyncio_running_task(PyObject *loop);
2067+
static inline void set_ts_asyncio_running_task(PyObject *loop, PyObject *task);
20662068

20672069
/* ----- Task._step wrapper */
20682070

@@ -2236,47 +2238,7 @@ enter_task(asyncio_state *state, PyObject *loop, PyObject *task)
22362238

22372239
assert(task == item);
22382240
Py_CLEAR(item);
2239-
2240-
// This block is needed to enable `asyncio.capture_call_graph()` API.
2241-
// We want to be enable debuggers and profilers to be able to quickly
2242-
// introspect the asyncio running state from another process.
2243-
// When we do that, we need to essentially traverse the address space
2244-
// of a Python process and understand what every Python thread in it is
2245-
// currently doing, mainly:
2246-
//
2247-
// * current frame
2248-
// * current asyncio task
2249-
//
2250-
// A naive solution would be to require profilers and debuggers to
2251-
// find the current task in the "_asynciomodule" module state, but
2252-
// unfortunately that would require a lot of complicated remote
2253-
// memory reads and logic, as Python's dict is a notoriously complex
2254-
// and ever-changing data structure.
2255-
//
2256-
// So the easier solution is to put a strong reference to the currently
2257-
// running `asyncio.Task` on the interpreter thread state (we already
2258-
// have some asyncio state there.)
2259-
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)_PyThreadState_GET();
2260-
if (ts->asyncio_running_loop == loop) {
2261-
// Protect from a situation when someone calls this method
2262-
// from another thread. This shouldn't ever happen though,
2263-
// as `enter_task` and `leave_task` can either be called by:
2264-
//
2265-
// - `asyncio.Task` itself, in `Task.__step()`. That method
2266-
// can only be called by the event loop itself.
2267-
//
2268-
// - third-party Task "from scratch" implementations, that
2269-
// our `capture_call_graph` API doesn't support anyway.
2270-
//
2271-
// That said, we still want to make sure we don't end up in
2272-
// a broken state, so we check that we're in the correct thread
2273-
// by comparing the *loop* argument to the event loop running
2274-
// in the current thread. If they match we know we're in the
2275-
// right thread, as asyncio event loops don't change threads.
2276-
assert(ts->asyncio_running_task == NULL);
2277-
ts->asyncio_running_task = Py_NewRef(task);
2278-
}
2279-
2241+
set_ts_asyncio_running_task(loop, task);
22802242
return 0;
22812243
}
22822244

@@ -2308,14 +2270,7 @@ leave_task(asyncio_state *state, PyObject *loop, PyObject *task)
23082270
// task was not found
23092271
return err_leave_task(Py_None, task);
23102272
}
2311-
2312-
// See the comment in `enter_task` for the explanation of why
2313-
// the following is needed.
2314-
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)_PyThreadState_GET();
2315-
if (ts->asyncio_running_loop == NULL || ts->asyncio_running_loop == loop) {
2316-
Py_CLEAR(ts->asyncio_running_task);
2317-
}
2318-
2273+
clear_ts_asyncio_running_task(loop);
23192274
return res;
23202275
}
23212276

@@ -2342,6 +2297,7 @@ swap_current_task(asyncio_state *state, PyObject *loop, PyObject *task)
23422297
{
23432298
PyObject *prev_task;
23442299

2300+
clear_ts_asyncio_running_task(loop);
23452301
if (task == Py_None) {
23462302
if (PyDict_Pop(state->current_tasks, loop, &prev_task) < 0) {
23472303
return NULL;
@@ -2361,9 +2317,63 @@ swap_current_task(asyncio_state *state, PyObject *loop, PyObject *task)
23612317
Py_BEGIN_CRITICAL_SECTION(current_tasks);
23622318
prev_task = swap_current_task_lock_held(current_tasks, loop, hash, task);
23632319
Py_END_CRITICAL_SECTION();
2320+
set_ts_asyncio_running_task(loop, task);
23642321
return prev_task;
23652322
}
23662323

2324+
static inline void
2325+
set_ts_asyncio_running_task(PyObject *loop, PyObject *task)
2326+
{
2327+
// We want to enable debuggers and profilers to be able to quickly
2328+
// introspect the asyncio running state from another process.
2329+
// When we do that, we need to essentially traverse the address space
2330+
// of a Python process and understand what every Python thread in it is
2331+
// currently doing, mainly:
2332+
//
2333+
// * current frame
2334+
// * current asyncio task
2335+
//
2336+
// A naive solution would be to require profilers and debuggers to
2337+
// find the current task in the "_asynciomodule" module state, but
2338+
// unfortunately that would require a lot of complicated remote
2339+
// memory reads and logic, as Python's dict is a notoriously complex
2340+
// and ever-changing data structure.
2341+
//
2342+
// So the easier solution is to put a strong reference to the currently
2343+
// running `asyncio.Task` on the current thread state (the current loop
2344+
// is also stored there.)
2345+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)_PyThreadState_GET();
2346+
if (ts->asyncio_running_loop == loop) {
2347+
// Protect from a situation when someone calls this method
2348+
// from another thread. This shouldn't ever happen though,
2349+
// as `enter_task` and `leave_task` can either be called by:
2350+
//
2351+
// - `asyncio.Task` itself, in `Task.__step()`. That method
2352+
// can only be called by the event loop itself.
2353+
//
2354+
// - third-party Task "from scratch" implementations, that
2355+
// our `capture_call_graph` API doesn't support anyway.
2356+
//
2357+
// That said, we still want to make sure we don't end up in
2358+
// a broken state, so we check that we're in the correct thread
2359+
// by comparing the *loop* argument to the event loop running
2360+
// in the current thread. If they match we know we're in the
2361+
// right thread, as asyncio event loops don't change threads.
2362+
assert(ts->asyncio_running_task == NULL);
2363+
ts->asyncio_running_task = Py_NewRef(task);
2364+
}
2365+
}
2366+
2367+
static inline void
2368+
clear_ts_asyncio_running_task(PyObject *loop)
2369+
{
2370+
// See comment in set_ts_asyncio_running_task() for details.
2371+
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)_PyThreadState_GET();
2372+
if (ts->asyncio_running_loop == NULL || ts->asyncio_running_loop == loop) {
2373+
Py_CLEAR(ts->asyncio_running_task);
2374+
}
2375+
}
2376+
23672377
/* ----- Task */
23682378

23692379
/*[clinic input]

0 commit comments

Comments
 (0)