Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix a data race on the garbage collector debug flag in free-threading builds:
:func:`gc.set_debug` and :func:`gc.get_debug` now access it atomically.
5 changes: 3 additions & 2 deletions Modules/gcmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "Python.h"
#include "pycore_gc.h"
#include "pycore_object.h" // _PyObject_IS_GC()
#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_INT_RELAXED()
#include "pycore_pystate.h" // _PyInterpreterState_GET()

typedef struct _gc_runtime_state GCState;
Expand Down Expand Up @@ -116,7 +117,7 @@ gc_set_debug_impl(PyObject *module, int flags)
/*[clinic end generated code: output=7c8366575486b228 input=5e5ce15e84fbed15]*/
{
GCState *gcstate = get_gc_state();
gcstate->debug = flags;
FT_ATOMIC_STORE_INT_RELAXED(gcstate->debug, flags);
Py_RETURN_NONE;
}

Expand All @@ -131,7 +132,7 @@ gc_get_debug_impl(PyObject *module)
/*[clinic end generated code: output=91242f3506cd1e50 input=91a101e1c3b98366]*/
{
GCState *gcstate = get_gc_state();
return gcstate->debug;
return FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);
}

/*[clinic input]
Expand Down
23 changes: 14 additions & 9 deletions Python/gc_free_threading.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include "pycore_interpframe.h" // _PyFrame_GetLocalsArray()
#include "pycore_list.h" // _PyList_GetItemRef()
#include "pycore_object_alloc.h" // _PyObject_MallocWithType()
#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_INT_RELAXED()
#include "pycore_pystate.h" // _PyThreadState_GET()
#include "pycore_tstate.h" // _PyThreadStateImpl
#include "pycore_tuple.h" // _PyTuple_MaybeUntrack()
Expand Down Expand Up @@ -1738,6 +1739,7 @@ delete_garbage(struct collection_state *state)
{
PyThreadState *tstate = _PyThreadState_GET();
GCState *gcstate = state->gcstate;
int debug = FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);

assert(!_PyErr_Occurred(tstate));

Expand All @@ -1760,7 +1762,7 @@ delete_garbage(struct collection_state *state)

state->collected++;

if (gcstate->debug & _PyGC_DEBUG_SAVEALL) {
if (debug & _PyGC_DEBUG_SAVEALL) {
assert(gcstate->garbage != NULL);
if (PyList_Append(gcstate->garbage, op) < 0) {
_PyErr_Clear(tstate);
Expand All @@ -1786,16 +1788,17 @@ handle_legacy_finalizers(struct collection_state *state)
{
GCState *gcstate = state->gcstate;
assert(gcstate->garbage != NULL);
int debug = FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);

PyObject *op;
while ((op = worklist_pop(&state->legacy_finalizers)) != NULL) {
state->uncollectable++;

if (gcstate->debug & _PyGC_DEBUG_UNCOLLECTABLE) {
if (debug & _PyGC_DEBUG_UNCOLLECTABLE) {
debug_cycle("uncollectable", op);
}

if ((gcstate->debug & _PyGC_DEBUG_SAVEALL) || has_legacy_finalizer(op)) {
if ((debug & _PyGC_DEBUG_SAVEALL) || has_legacy_finalizer(op)) {
if (PyList_Append(gcstate->garbage, op) < 0) {
PyErr_Clear();
}
Expand Down Expand Up @@ -2146,7 +2149,7 @@ gc_collect_internal(PyInterpreterState *interp, struct collection_state *state,
bool needs_resurrection_check = state->objs_to_decref.head != 0;

// Print debugging information.
if (interp->gc.debug & _PyGC_DEBUG_COLLECTABLE) {
if (FT_ATOMIC_LOAD_INT_RELAXED(interp->gc.debug) & _PyGC_DEBUG_COLLECTABLE) {
needs_resurrection_check = true;
PyObject *op;
WORKSTACK_FOR_EACH(&state->unreachable, op) {
Expand Down Expand Up @@ -2260,7 +2263,8 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
invoke_gc_callback(tstate, "start", generation, 0, 0, 0, 0.0);
}

if (gcstate->debug & _PyGC_DEBUG_STATS) {
int debug = FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);
if (debug & _PyGC_DEBUG_STATS) {
PySys_WriteStderr("gc: collecting generation %d...\n", generation);
show_stats_each_generations(gcstate);
}
Expand All @@ -2287,7 +2291,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
(void)PyTime_PerfCounterRaw(&stop);
double duration = PyTime_AsSecondsDouble(stop - start);

if (gcstate->debug & _PyGC_DEBUG_STATS) {
if (debug & _PyGC_DEBUG_STATS) {
PySys_WriteStderr(
"gc: done, %zd unreachable, %zd uncollectable, %.4fs elapsed\n",
n+m, n, duration);
Expand Down Expand Up @@ -2618,10 +2622,11 @@ void
_PyGC_DumpShutdownStats(PyInterpreterState *interp)
{
GCState *gcstate = &interp->gc;
if (!(gcstate->debug & _PyGC_DEBUG_SAVEALL)
int debug = FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);
if (!(debug & _PyGC_DEBUG_SAVEALL)
&& gcstate->garbage != NULL && PyList_GET_SIZE(gcstate->garbage) > 0) {
const char *message;
if (gcstate->debug & _PyGC_DEBUG_UNCOLLECTABLE) {
if (debug & _PyGC_DEBUG_UNCOLLECTABLE) {
message = "gc: %zd uncollectable objects at shutdown";
}
else {
Expand All @@ -2637,7 +2642,7 @@ _PyGC_DumpShutdownStats(PyInterpreterState *interp)
{
PyErr_FormatUnraisable("Exception ignored in GC shutdown");
}
if (gcstate->debug & _PyGC_DEBUG_UNCOLLECTABLE) {
if (debug & _PyGC_DEBUG_UNCOLLECTABLE) {
PyObject *repr = NULL, *bytes = NULL;
repr = PyObject_Repr(gcstate->garbage);
if (!repr || !(bytes = PyUnicode_EncodeFSDefault(repr))) {
Expand Down
Loading