From 3e24b32244439911bdcf89b70ea61fe8411dd0e7 Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sat, 1 Aug 2026 00:54:16 +0800 Subject: [PATCH 1/6] fix ident mutex issue --- Modules/_threadmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c index e999fe20287e2d6..199e4ac3db723bf 100644 --- a/Modules/_threadmodule.c +++ b/Modules/_threadmodule.c @@ -2413,7 +2413,7 @@ thread_shutdown(PyObject *self, PyObject *args) struct llist_node *node; llist_for_each_safe(node, &state->shutdown_handles) { ThreadHandle *cur = llist_data(node, ThreadHandle, shutdown_node); - if (cur->ident != ident) { + if (ThreadHandle_ident(cur) != ident) { ThreadHandle_incref(cur); handle = cur; break; From b8d8dfca869ad0606ad77f1c9fd1a2f5c637b904 Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sun, 2 Aug 2026 17:50:39 +0800 Subject: [PATCH 2/6] add unit test case --- .../test_free_threading/test_threading.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Lib/test/test_free_threading/test_threading.py b/Lib/test/test_free_threading/test_threading.py index b5a5ca272b9405a..4faf3499ce3e001 100644 --- a/Lib/test/test_free_threading/test_threading.py +++ b/Lib/test/test_free_threading/test_threading.py @@ -22,5 +22,40 @@ def mutate_thread(): threading_helper.run_concurrently([repr_thread, mutate_thread]) +class TestShutdown(unittest.TestCase): + def test_shutdown_race(self): + import _thread + import threading + + ITERATIONS = 1_000 + STARTUP_THREADS = 4 + + def shutdown_worker(): + for _ in range(ITERATIONS): + try: + _thread._shutdown() + except RuntimeError: + pass + + def startup_worker(): + handles = [] + for _ in range(ITERATIONS): + handle = _thread.start_joinable_thread( + lambda: None, + daemon=False, + ) + handles.append(handle) + for handle in handles: + handle.join() + + workers = [ + threading.Thread(target=shutdown_worker, daemon=True), + *[threading.Thread( + target=startup_worker, daemon=True) for _ in range(STARTUP_THREADS)], + ] + + with threading_helper.start_threads(workers): + pass + if __name__ == "__main__": unittest.main() From 266ced261fc8e6221b982ff38b4f95e11f983091 Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sun, 2 Aug 2026 17:55:54 +0800 Subject: [PATCH 3/6] add blurb --- .../2026-08-02-17-55-29.gh-issue-154937.m9IBba.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-02-17-55-29.gh-issue-154937.m9IBba.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-02-17-55-29.gh-issue-154937.m9IBba.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-02-17-55-29.gh-issue-154937.m9IBba.rst new file mode 100644 index 000000000000000..475dc8358a8d811 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-02-17-55-29.gh-issue-154937.m9IBba.rst @@ -0,0 +1,3 @@ +Fix a data race on thread handle identifiers when ``_thread._shutdown()`` +runs concurrently with the startup of non-daemon threads in the +:term:`free-threaded build`. From 4384239289e9d9bd6884151e6bdd8f9d1c24704a Mon Sep 17 00:00:00 2001 From: edward_xu Date: Sun, 2 Aug 2026 18:09:23 +0800 Subject: [PATCH 4/6] follow copliot comment and reduce concurrent threads count --- .../test_free_threading/test_threading.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_free_threading/test_threading.py b/Lib/test/test_free_threading/test_threading.py index 4faf3499ce3e001..d51e7d2943b3014 100644 --- a/Lib/test/test_free_threading/test_threading.py +++ b/Lib/test/test_free_threading/test_threading.py @@ -38,24 +38,28 @@ def shutdown_worker(): pass def startup_worker(): - handles = [] for _ in range(ITERATIONS): handle = _thread.start_joinable_thread( lambda: None, daemon=False, ) - handles.append(handle) - for handle in handles: handle.join() + # The workers must be daemon threads so _thread._shutdown() ignores them + # and only scans the non-daemon handles created by startup_worker(). workers = [ - threading.Thread(target=shutdown_worker, daemon=True), - *[threading.Thread( - target=startup_worker, daemon=True) for _ in range(STARTUP_THREADS)], + threading.Thread(target=shutdown_worker, daemon=True), + *[ + threading.Thread(target=startup_worker, daemon=True) + for _ in range(STARTUP_THREADS) + ], ] - with threading_helper.start_threads(workers): - pass + with threading_helper.catch_threading_exception() as cm: + with threading_helper.start_threads(workers): + pass + if cm.exc_value is not None: + raise cm.exc_value if __name__ == "__main__": unittest.main() From 3efdf4fd0198b18e92f2b13ef6521fb06a15738a Mon Sep 17 00:00:00 2001 From: edward_xu Date: Mon, 3 Aug 2026 08:17:02 +0800 Subject: [PATCH 5/6] fix the import and newline for comment --- Lib/test/test_free_threading/test_threading.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_free_threading/test_threading.py b/Lib/test/test_free_threading/test_threading.py index d51e7d2943b3014..e49d8c4d120816f 100644 --- a/Lib/test/test_free_threading/test_threading.py +++ b/Lib/test/test_free_threading/test_threading.py @@ -1,4 +1,6 @@ import unittest +import threading +import _thread from test.support import threading_helper threading_helper.requires_working_threading(module=True) @@ -7,7 +9,6 @@ class TestRlock(unittest.TestCase): def test_repr_race(self): # gh-153292 - import _thread r = _thread.RLock() def repr_thread(): @@ -24,9 +25,6 @@ def mutate_thread(): class TestShutdown(unittest.TestCase): def test_shutdown_race(self): - import _thread - import threading - ITERATIONS = 1_000 STARTUP_THREADS = 4 @@ -61,5 +59,6 @@ def startup_worker(): if cm.exc_value is not None: raise cm.exc_value + if __name__ == "__main__": unittest.main() From 2f0a2ebff4daaec0fe84ce61231d932bc75c7184 Mon Sep 17 00:00:00 2001 From: edward_xu Date: Mon, 3 Aug 2026 22:44:12 +0800 Subject: [PATCH 6/6] remove the heavy unit test which launches 4*1_000 os threads --- .../test_free_threading/test_threading.py | 40 +------------------ 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/Lib/test/test_free_threading/test_threading.py b/Lib/test/test_free_threading/test_threading.py index e49d8c4d120816f..b5a5ca272b9405a 100644 --- a/Lib/test/test_free_threading/test_threading.py +++ b/Lib/test/test_free_threading/test_threading.py @@ -1,6 +1,4 @@ import unittest -import threading -import _thread from test.support import threading_helper threading_helper.requires_working_threading(module=True) @@ -9,6 +7,7 @@ class TestRlock(unittest.TestCase): def test_repr_race(self): # gh-153292 + import _thread r = _thread.RLock() def repr_thread(): @@ -23,42 +22,5 @@ def mutate_thread(): threading_helper.run_concurrently([repr_thread, mutate_thread]) -class TestShutdown(unittest.TestCase): - def test_shutdown_race(self): - ITERATIONS = 1_000 - STARTUP_THREADS = 4 - - def shutdown_worker(): - for _ in range(ITERATIONS): - try: - _thread._shutdown() - except RuntimeError: - pass - - def startup_worker(): - for _ in range(ITERATIONS): - handle = _thread.start_joinable_thread( - lambda: None, - daemon=False, - ) - handle.join() - - # The workers must be daemon threads so _thread._shutdown() ignores them - # and only scans the non-daemon handles created by startup_worker(). - workers = [ - threading.Thread(target=shutdown_worker, daemon=True), - *[ - threading.Thread(target=startup_worker, daemon=True) - for _ in range(STARTUP_THREADS) - ], - ] - - with threading_helper.catch_threading_exception() as cm: - with threading_helper.start_threads(workers): - pass - if cm.exc_value is not None: - raise cm.exc_value - - if __name__ == "__main__": unittest.main()