Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Lib/test/test_free_threading/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from test import support
from test.support import threading_helper
from test.support.script_helper import assert_python_ok


class MyObj:
Expand Down Expand Up @@ -110,6 +111,35 @@ def run(self):

self.assertTrue(C().run())

def test_debug_collectable_python_stderr(self):

@johng johng Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure of precedence with this type of test, it will only be useful in debug mode

# The DEBUG_COLLECTABLE output is written with sys.stderr.write(),
# which may run Python code or wait for another thread (e.g. for the
# lock of a buffered stream), so it must not be written while the
# world is stopped. Debug builds assert that the world is not stopped
# when Python code runs, so use a sys.stderr written in Python.
code = """if 1:
import gc
import io
import sys

class Stderr(io.StringIO):
def write(self, s):
return super().write(s)

sys.stderr = Stderr()
gc.set_debug(gc.DEBUG_COLLECTABLE)
a = []
a.append(a)
del a
gc.collect()
gc.set_debug(0)
output = sys.stderr.getvalue()
sys.stderr = sys.__stderr__
print(output)
"""
_, out, _ = assert_python_ok("-c", code)
self.assertRegex(out, b"gc: collectable <list 0x[0-9A-Fa-f]+>")

def test_merge_brc_queue_of_detached_thread(self):
# GH-157838: objects queued for merging by a thread that is detached
# (blocked in a lock acquire, sleep, etc.) are merged and freed on its
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix a deadlock in the garbage collector of the :term:`free-threaded build`
when :data:`gc.DEBUG_COLLECTABLE` is set and another thread is writing to
:data:`sys.stderr`. The debug output is now written after the other threads
are resumed.
22 changes: 13 additions & 9 deletions Python/gc_free_threading.c
Original file line number Diff line number Diff line change
Expand Up @@ -2158,20 +2158,24 @@ gc_collect_internal(PyInterpreterState *interp, struct collection_state *state,
// Deallocations after the refcount merge may run Python code.
bool needs_resurrection_check = state->objs_to_decref.head != 0;

// Print debugging information.
if (FT_ATOMIC_LOAD_INT_RELAXED(interp->gc.debug) & _PyGC_DEBUG_COLLECTABLE) {
needs_resurrection_check = true;
PyObject *op;
WORKSTACK_FOR_EACH(&state->unreachable, op) {
debug_cycle("collectable", op);
}
}
// The debug output is written with sys.stderr.write(), which may run
// Python code, so it is printed below after the world is started.
bool debug_print = (FT_ATOMIC_LOAD_INT_RELAXED(interp->gc.debug) &
_PyGC_DEBUG_COLLECTABLE) != 0;

// Find weakref callbacks and pending finalizers (but do not call them).
bool has_finalizers = prepare_finalization(state, &needs_resurrection_check);
if (needs_resurrection_check) {
if (needs_resurrection_check || debug_print) {
_PyEval_StartTheWorld(interp);

// Print debugging information.
if (debug_print) {
PyObject *op;
WORKSTACK_FOR_EACH(&state->unreachable, op) {
debug_cycle("collectable", op);
}
}

// Deallocate any object from the refcount merge step
cleanup_worklist(&state->objs_to_decref);

Expand Down
Loading