Skip to content

Commit 495a763

Browse files
committed
pythongh-117439: Make refleak checking thread-safe without the GIL
This keeps track of the per-thread total reference count operations in PyThreadState in the free-threaded builds. The count is merged into the interpreter's total when the thread exits.
1 parent 9dae05e commit 495a763

File tree

9 files changed

+60
-44
lines changed

9 files changed

+60
-44
lines changed

Include/internal/pycore_object.h

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ PyAPI_FUNC(void) _Py_NO_RETURN _Py_FatalRefcountErrorFunc(
8686
built against the pre-3.12 stable ABI. */
8787
PyAPI_DATA(Py_ssize_t) _Py_RefTotal;
8888

89-
extern void _Py_AddRefTotal(PyInterpreterState *, Py_ssize_t);
90-
extern void _Py_IncRefTotal(PyInterpreterState *);
91-
extern void _Py_DecRefTotal(PyInterpreterState *);
89+
extern void _Py_AddRefTotal(PyThreadState *, Py_ssize_t);
90+
extern void _Py_IncRefTotal(PyThreadState *);
91+
extern void _Py_DecRefTotal(PyThreadState *);
9292

9393
# define _Py_DEC_REFTOTAL(interp) \
9494
interp->object_state.reftotal--
@@ -101,7 +101,7 @@ static inline void _Py_RefcntAdd(PyObject* op, Py_ssize_t n)
101101
return;
102102
}
103103
#ifdef Py_REF_DEBUG
104-
_Py_AddRefTotal(_PyInterpreterState_GET(), n);
104+
_Py_AddRefTotal(_PyThreadState_GET(), n);
105105
#endif
106106
#if !defined(Py_GIL_DISABLED)
107107
op->ob_refcnt += n;
@@ -394,7 +394,7 @@ _Py_TryIncrefFast(PyObject *op) {
394394
_Py_INCREF_STAT_INC();
395395
_Py_atomic_store_uint32_relaxed(&op->ob_ref_local, local);
396396
#ifdef Py_REF_DEBUG
397-
_Py_IncRefTotal(_PyInterpreterState_GET());
397+
_Py_IncRefTotal(_PyThreadState_GET());
398398
#endif
399399
return 1;
400400
}
@@ -417,7 +417,7 @@ _Py_TryIncRefShared(PyObject *op)
417417
&shared,
418418
shared + (1 << _Py_REF_SHARED_SHIFT))) {
419419
#ifdef Py_REF_DEBUG
420-
_Py_IncRefTotal(_PyInterpreterState_GET());
420+
_Py_IncRefTotal(_PyThreadState_GET());
421421
#endif
422422
_Py_INCREF_STAT_INC();
423423
return 1;

Include/internal/pycore_tstate.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ typedef struct _PyThreadStateImpl {
3838
struct _brc_thread_state brc;
3939
#endif
4040

41+
#if defined(Py_REF_DEBUG) && defined(Py_GIL_DISABLED)
42+
// per-thread count of refcount operations
43+
Py_ssize_t reftotal;
44+
#endif
45+
4146
} _PyThreadStateImpl;
4247

4348

Objects/bytesobject.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3084,7 +3084,7 @@ _PyBytes_Resize(PyObject **pv, Py_ssize_t newsize)
30843084
PyObject_Realloc(v, PyBytesObject_SIZE + newsize);
30853085
if (*pv == NULL) {
30863086
#ifdef Py_REF_DEBUG
3087-
_Py_DecRefTotal(_PyInterpreterState_GET());
3087+
_Py_DecRefTotal(_PyThreadState_GET());
30883088
#endif
30893089
PyObject_Free(v);
30903090
PyErr_NoMemory();

Objects/dictobject.c

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ dictkeys_incref(PyDictKeysObject *dk)
441441
return;
442442
}
443443
#ifdef Py_REF_DEBUG
444-
_Py_IncRefTotal(_PyInterpreterState_GET());
444+
_Py_IncRefTotal(_PyThreadState_GET());
445445
#endif
446446
INCREF_KEYS(dk);
447447
}
@@ -454,7 +454,7 @@ dictkeys_decref(PyInterpreterState *interp, PyDictKeysObject *dk, bool use_qsbr)
454454
}
455455
assert(dk->dk_refcnt > 0);
456456
#ifdef Py_REF_DEBUG
457-
_Py_DecRefTotal(_PyInterpreterState_GET());
457+
_Py_DecRefTotal(_PyThreadState_GET());
458458
#endif
459459
if (DECREF_KEYS(dk) == 1) {
460460
if (DK_IS_UNICODE(dk)) {
@@ -781,7 +781,7 @@ new_keys_object(PyInterpreterState *interp, uint8_t log2_size, bool unicode)
781781
}
782782
}
783783
#ifdef Py_REF_DEBUG
784-
_Py_IncRefTotal(_PyInterpreterState_GET());
784+
_Py_IncRefTotal(_PyThreadState_GET());
785785
#endif
786786
dk->dk_refcnt = 1;
787787
dk->dk_log2_size = log2_size;
@@ -977,7 +977,7 @@ clone_combined_dict_keys(PyDictObject *orig)
977977
we have it now; calling dictkeys_incref would be an error as
978978
keys->dk_refcnt is already set to 1 (after memcpy). */
979979
#ifdef Py_REF_DEBUG
980-
_Py_IncRefTotal(_PyInterpreterState_GET());
980+
_Py_IncRefTotal(_PyThreadState_GET());
981981
#endif
982982
return keys;
983983
}
@@ -2002,7 +2002,7 @@ dictresize(PyInterpreterState *interp, PyDictObject *mp,
20022002

20032003
if (oldkeys != Py_EMPTY_KEYS) {
20042004
#ifdef Py_REF_DEBUG
2005-
_Py_DecRefTotal(_PyInterpreterState_GET());
2005+
_Py_DecRefTotal(_PyThreadState_GET());
20062006
#endif
20072007
assert(oldkeys->dk_kind != DICT_KEYS_SPLIT);
20082008
assert(oldkeys->dk_refcnt == 1);

Objects/object.c

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -73,21 +73,14 @@ get_legacy_reftotal(void)
7373
interp->object_state.reftotal
7474

7575
static inline void
76-
reftotal_increment(PyInterpreterState *interp)
76+
reftotal_add(PyThreadState *tstate, Py_ssize_t n)
7777
{
78-
REFTOTAL(interp)++;
79-
}
80-
81-
static inline void
82-
reftotal_decrement(PyInterpreterState *interp)
83-
{
84-
REFTOTAL(interp)--;
85-
}
86-
87-
static inline void
88-
reftotal_add(PyInterpreterState *interp, Py_ssize_t n)
89-
{
90-
REFTOTAL(interp) += n;
78+
#ifdef Py_GIL_DISABLED
79+
_PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)tstate;
80+
tstate_impl->reftotal += n;
81+
#else
82+
REFTOTAL(tstate->interp) += n;
83+
#endif
9184
}
9285

9386
static inline Py_ssize_t get_global_reftotal(_PyRuntimeState *);
@@ -117,7 +110,14 @@ get_reftotal(PyInterpreterState *interp)
117110
{
118111
/* For a single interpreter, we ignore the legacy _Py_RefTotal,
119112
since we can't determine which interpreter updated it. */
120-
return REFTOTAL(interp);
113+
Py_ssize_t total = REFTOTAL(interp);
114+
#ifdef Py_GIL_DISABLED
115+
for (PyThreadState *p = interp->threads.head; p != NULL; p = p->next) {
116+
/* This may race with other threads modifications to their reftotal */
117+
total += ((_PyThreadStateImpl *)p)->reftotal;
118+
}
119+
#endif
120+
return total;
121121
}
122122

123123
static inline Py_ssize_t
@@ -129,7 +129,7 @@ get_global_reftotal(_PyRuntimeState *runtime)
129129
HEAD_LOCK(&_PyRuntime);
130130
PyInterpreterState *interp = PyInterpreterState_Head();
131131
for (; interp != NULL; interp = PyInterpreterState_Next(interp)) {
132-
total += REFTOTAL(interp);
132+
total += get_reftotal(interp);
133133
}
134134
HEAD_UNLOCK(&_PyRuntime);
135135

@@ -222,32 +222,32 @@ _Py_NegativeRefcount(const char *filename, int lineno, PyObject *op)
222222
void
223223
_Py_INCREF_IncRefTotal(void)
224224
{
225-
reftotal_increment(_PyInterpreterState_GET());
225+
reftotal_add(_PyThreadState_GET(), 1);
226226
}
227227

228228
/* This is used strictly by Py_DECREF(). */
229229
void
230230
_Py_DECREF_DecRefTotal(void)
231231
{
232-
reftotal_decrement(_PyInterpreterState_GET());
232+
reftotal_add(_PyThreadState_GET(), -1);
233233
}
234234

235235
void
236-
_Py_IncRefTotal(PyInterpreterState *interp)
236+
_Py_IncRefTotal(PyThreadState *tstate)
237237
{
238-
reftotal_increment(interp);
238+
reftotal_add(tstate, 1);
239239
}
240240

241241
void
242-
_Py_DecRefTotal(PyInterpreterState *interp)
242+
_Py_DecRefTotal(PyThreadState *tstate)
243243
{
244-
reftotal_decrement(interp);
244+
reftotal_add(tstate, -1);
245245
}
246246

247247
void
248-
_Py_AddRefTotal(PyInterpreterState *interp, Py_ssize_t n)
248+
_Py_AddRefTotal(PyThreadState *tstate, Py_ssize_t n)
249249
{
250-
reftotal_add(interp, n);
250+
reftotal_add(tstate, n);
251251
}
252252

253253
/* This includes the legacy total
@@ -267,7 +267,10 @@ _Py_GetLegacyRefTotal(void)
267267
Py_ssize_t
268268
_PyInterpreterState_GetRefTotal(PyInterpreterState *interp)
269269
{
270-
return get_reftotal(interp);
270+
HEAD_LOCK(&_PyRuntime);
271+
Py_ssize_t total = get_reftotal(interp);
272+
HEAD_UNLOCK(&_PyRuntime);
273+
return total;
271274
}
272275

273276
#endif /* Py_REF_DEBUG */
@@ -345,7 +348,7 @@ _Py_DecRefSharedDebug(PyObject *o, const char *filename, int lineno)
345348

346349
if (should_queue) {
347350
#ifdef Py_REF_DEBUG
348-
_Py_IncRefTotal(_PyInterpreterState_GET());
351+
_Py_IncRefTotal(_PyThreadState_GET());
349352
#endif
350353
_Py_brc_queue_object(o);
351354
}
@@ -405,7 +408,7 @@ _Py_ExplicitMergeRefcount(PyObject *op, Py_ssize_t extra)
405408
&shared, new_shared));
406409

407410
#ifdef Py_REF_DEBUG
408-
_Py_AddRefTotal(_PyInterpreterState_GET(), extra);
411+
_Py_AddRefTotal(_PyThreadState_GET(), extra);
409412
#endif
410413

411414
_Py_atomic_store_uint32_relaxed(&op->ob_ref_local, 0);
@@ -2388,7 +2391,7 @@ void
23882391
_Py_NewReference(PyObject *op)
23892392
{
23902393
#ifdef Py_REF_DEBUG
2391-
reftotal_increment(_PyInterpreterState_GET());
2394+
_Py_IncRefTotal(_PyThreadState_GET());
23922395
#endif
23932396
new_reference(op);
23942397
}

Objects/tupleobject.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,7 @@ _PyTuple_Resize(PyObject **pv, Py_ssize_t newsize)
946946
if (sv == NULL) {
947947
*pv = NULL;
948948
#ifdef Py_REF_DEBUG
949-
_Py_DecRefTotal(_PyInterpreterState_GET());
949+
_Py_DecRefTotal(_PyThreadState_GET());
950950
#endif
951951
PyObject_GC_Del(v);
952952
return -1;

Objects/unicodeobject.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14977,7 +14977,7 @@ _PyUnicode_InternInPlace(PyInterpreterState *interp, PyObject **p)
1497714977
decrements to these objects will not be registered so they
1497814978
need to be accounted for in here. */
1497914979
for (Py_ssize_t i = 0; i < Py_REFCNT(s) - 2; i++) {
14980-
_Py_DecRefTotal(_PyInterpreterState_GET());
14980+
_Py_DecRefTotal(_PyThreadState_GET());
1498114981
}
1498214982
#endif
1498314983
_Py_SetImmortal(s);

Python/gc_free_threading.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ merge_refcount(PyObject *op, Py_ssize_t extra)
168168
refcount += extra;
169169

170170
#ifdef Py_REF_DEBUG
171-
_Py_AddRefTotal(_PyInterpreterState_GET(), extra);
171+
_Py_AddRefTotal(_PyThreadState_GET(), extra);
172172
#endif
173173

174174
// No atomics necessary; all other threads in this interpreter are paused.
@@ -307,7 +307,7 @@ merge_queued_objects(_PyThreadStateImpl *tstate, struct collection_state *state)
307307
// decref and deallocate the object once we start the world again.
308308
op->ob_ref_shared += (1 << _Py_REF_SHARED_SHIFT);
309309
#ifdef Py_REF_DEBUG
310-
_Py_IncRefTotal(_PyInterpreterState_GET());
310+
_Py_IncRefTotal(_PyThreadState_GET());
311311
#endif
312312
worklist_push(&state->objs_to_decref, op);
313313
}

Python/pystate.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1689,6 +1689,14 @@ tstate_delete_common(PyThreadState *tstate)
16891689
decrement_stoptheworld_countdown(&runtime->stoptheworld);
16901690
}
16911691
}
1692+
1693+
#if defined(Py_REF_DEBUG) && defined(Py_GIL_DISABLED)
1694+
// Add our portion of the total refcount to the interpreter's total.
1695+
_PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)tstate;
1696+
tstate->interp->object_state.reftotal += tstate_impl->reftotal;
1697+
tstate_impl->reftotal = 0;
1698+
#endif
1699+
16921700
HEAD_UNLOCK(runtime);
16931701

16941702
#ifdef Py_GIL_DISABLED

0 commit comments

Comments
 (0)