diff --git a/Lib/test/test_free_threading/test_gc.py b/Lib/test/test_free_threading/test_gc.py index 8b45b6e2150c28..54cb8ccee395e3 100644 --- a/Lib/test/test_free_threading/test_gc.py +++ b/Lib/test/test_free_threading/test_gc.py @@ -94,6 +94,35 @@ def evil(): thread.start() thread.join() + def test_get_count(self): + class CyclicReference: + def __init__(self): + self.ref = self + + NUM_ALLOCATORS = 7 + NUM_READERS = 1 + NUM_THREADS = NUM_ALLOCATORS + NUM_READERS + NUM_ITERS = 200_000 + + barrier = threading.Barrier(NUM_THREADS) + + def allocator(): + barrier.wait() + for _ in range(NUM_ITERS): + CyclicReference() + + + def reader(): + barrier.wait() + for _ in range(NUM_ITERS): + gc.get_count() + + threads = [Thread(target=allocator) for _ in range(NUM_ALLOCATORS)] + threads.extend(Thread(target=reader) for _ in range(NUM_READERS)) + + with threading_helper.start_threads(threads): + pass + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-26-00-06-30.gh-issue-150411.u-d-_5.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-26-00-06-30.gh-issue-150411.u-d-_5.rst new file mode 100644 index 00000000000000..5b19a4fff5ddc7 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-26-00-06-30.gh-issue-150411.u-d-_5.rst @@ -0,0 +1,2 @@ +Fix a data race in the free-threaded build when :func:`gc.get_count` reads +the young generation allocation count while another thread updates it. diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c index 12f93ac0fdea14..e13acc859cab5a 100644 --- a/Modules/gcmodule.c +++ b/Modules/gcmodule.c @@ -230,9 +230,9 @@ gc_get_count_impl(PyObject *module) gcstate->generations[2].count); #else return Py_BuildValue("(iii)", - gcstate->young.count, - gcstate->old[0].count, - gcstate->old[1].count); + _Py_atomic_load_int_relaxed(&gcstate->young.count), + _Py_atomic_load_int_relaxed(&gcstate->old[0].count), + _Py_atomic_load_int_relaxed(&gcstate->old[1].count)); #endif }