Skip to content

Commit 12ea214

Browse files
Record the owning thread state in register_task() only and rework test
1 parent 5a64c43 commit 12ea214

2 files changed

Lines changed: 46 additions & 54 deletions

File tree

‎Lib/test/test_asyncio/test_free_threading.py‎

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -116,62 +116,58 @@ def runner():
116116
support.gc_collect()
117117
self.assertIsNone(wr())
118118

119-
def test_lingering_task_different_thread_finalized(self) -> None:
120-
tls = threading.local()
119+
def test_unregister_lingering_tasks_of_exited_thread(self) -> None:
120+
# gh-159449: a new thread can get the same OS thread id as
121+
# an exited thread whose tasks linger on the interpreter's list.
122+
class InertLoop:
123+
def get_debug(self):
124+
return False
121125

122-
async def never_finishes():
123-
await asyncio.Future()
126+
def is_running(self):
127+
return False
128+
129+
def call_soon(self, *args, **kwargs):
130+
pass
131+
132+
loop = InertLoop()
133+
done = threading.Event()
134+
135+
async def coro():
136+
pass
124137

125-
def make_pending_tasks(loop, count):
138+
def worker(handoff):
139+
while handoff:
140+
for task in handoff.pop():
141+
# complete the task right away, unlike Task.cancel()
142+
asyncio.Future.cancel(task)
126143
tasks = []
127-
for _ in range(count):
128-
task = loop.create_task(never_finishes())
144+
for _ in range(32):
145+
c = coro()
146+
task = self.factory(loop, c)
147+
c.close()
129148
task._log_destroy_pending = False
130149
tasks.append(task)
131-
return tasks
132-
133-
def worker(batches):
134-
loop = asyncio.new_event_loop()
135-
loop.set_task_factory(self.factory)
136-
tls_tasks = make_pending_tasks(loop, 8)
137-
for task in tls_tasks:
138-
task.get_coro().close()
139-
tls.tasks = tls_tasks
140-
batches.append((loop, make_pending_tasks(loop, 8)))
150+
handoff.append(tasks)
141151

142-
stop = threading.Event()
152+
def spawner():
153+
handoff = []
154+
for _ in range(100):
155+
thread = Thread(target=worker, args=(handoff,))
156+
thread.start()
157+
thread.join()
143158

144159
def inspector():
145-
loop = asyncio.new_event_loop()
146-
try:
147-
while not stop.is_set():
148-
asyncio.all_tasks(loop)
149-
finally:
150-
loop.close()
160+
while not done.is_set():
161+
asyncio.all_tasks(loop)
151162

152163
inspectors = [Thread(target=inspector) for _ in range(2)]
153164
with threading_helper.start_threads(inspectors):
154165
try:
155-
for _ in range(15):
156-
batches = []
157-
workers = [Thread(target=worker, args=(batches,)) for _ in range(4)]
158-
with threading_helper.start_threads(workers):
159-
pass
160-
161-
def cleanup(batch):
162-
loop, tasks = batch
163-
for task in tasks:
164-
task.cancel()
165-
task.get_coro().close()
166-
tasks.clear()
167-
loop.close()
168-
169-
cleaners = [Thread(target=cleanup, args=(b,)) for b in batches]
170-
with threading_helper.start_threads(cleaners):
171-
pass
172-
support.gc_collect()
166+
spawners = [Thread(target=spawner) for _ in range(4)]
167+
with threading_helper.start_threads(spawners):
168+
pass
173169
finally:
174-
stop.set()
170+
done.set()
175171

176172
def test_run_coroutine_threadsafe(self) -> None:
177173
results = []

‎Modules/_asynciomodule.c‎

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ typedef struct TaskObj {
6868
PyObject *task_context;
6969
struct llist_node task_node;
7070
#ifdef Py_GIL_DISABLED
71-
// thread id of the thread where this task was created
71+
// id of the thread state whose asyncio_tasks_head the task was
72+
// registered on; unlike _Py_ThreadId() it is never reused
7273
uint64_t task_tstate_id;
7374
#endif
7475
} TaskObj;
@@ -2208,14 +2209,14 @@ unregister_task(TaskObj *task)
22082209
{
22092210
#ifdef Py_GIL_DISABLED
22102211
PyThreadState *tstate = _PyThreadState_GET();
2211-
// check if we are in the same thread
2212-
// if so, we can avoid locking
2213-
if (task->task_tstate_id == tstate->id && !tstate->_status.finalizing) {
2212+
// The task can only be on this thread's list if this thread state
2213+
// registered it, in which case no locking is needed.
2214+
if (task->task_tstate_id == tstate->id) {
22142215
unregister_task_safe(task);
22152216
}
22162217
else {
2217-
// we are in a different thread
2218-
// stop the world then check and remove the task
2218+
// The task is on another thread's list or on the interpreter's
2219+
// list of lingering tasks: stop the world to remove it.
22192220
_PyEval_StopTheWorld(tstate->interp);
22202221
unregister_task_safe(task);
22212222
_PyEval_StartTheWorld(tstate->interp);
@@ -2343,11 +2344,6 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
23432344
}
23442345

23452346
Py_CLEAR(self->task_fut_waiter);
2346-
#ifdef Py_GIL_DISABLED
2347-
if (self->task_node.next == NULL) {
2348-
self->task_tstate_id = _PyThreadState_GET()->id;
2349-
}
2350-
#endif
23512347
self->task_must_cancel = 0;
23522348
self->task_log_destroy_pending = 1;
23532349
self->task_num_cancels_requested = 0;

0 commit comments

Comments
 (0)