Skip to content

Commit df73179

Browse files
authored
gh-111926: Make weakrefs thread-safe in free-threaded builds (#117168)
Most mutable data is protected by a striped lock that is keyed on the referenced object's address. The weakref's hash is protected using the weakref's per-object lock. Note that this only affects free-threaded builds. Apart from some minor refactoring, the added code is all either gated by `ifdef`s or is a no-op (e.g. `Py_BEGIN_CRITICAL_SECTION`).
1 parent e16062d commit df73179

17 files changed

+491
-327
lines changed

Diff for: Include/cpython/weakrefobject.h

+8
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ struct _PyWeakReference {
3030
PyWeakReference *wr_prev;
3131
PyWeakReference *wr_next;
3232
vectorcallfunc vectorcall;
33+
34+
#ifdef Py_GIL_DISABLED
35+
/* Pointer to the lock used when clearing in free-threaded builds.
36+
* Normally this can be derived from wr_object, but in some cases we need
37+
* to lock after wr_object has been set to Py_None.
38+
*/
39+
struct _PyMutex *weakrefs_lock;
40+
#endif
3341
};
3442

3543
Py_DEPRECATED(3.13) static inline PyObject* PyWeakref_GET_OBJECT(PyObject *ref_obj)

Diff for: Include/internal/pycore_interp.h

+7
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ struct _stoptheworld_state {
5959
PyThreadState *requester; // Thread that requested the pause (may be NULL).
6060
};
6161

62+
#ifdef Py_GIL_DISABLED
63+
// This should be prime but otherwise the choice is arbitrary. A larger value
64+
// increases concurrency at the expense of memory.
65+
# define NUM_WEAKREF_LIST_LOCKS 127
66+
#endif
67+
6268
/* cross-interpreter data registry */
6369

6470
/* Tracks some rare events per-interpreter, used by the optimizer to turn on/off
@@ -203,6 +209,7 @@ struct _is {
203209
#if defined(Py_GIL_DISABLED)
204210
struct _mimalloc_interp_state mimalloc;
205211
struct _brc_state brc; // biased reference counting state
212+
PyMutex weakref_locks[NUM_WEAKREF_LIST_LOCKS];
206213
#endif
207214

208215
// Per-interpreter state for the obmalloc allocator. For the main

Diff for: Include/internal/pycore_object.h

+37-3
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ _Py_TryIncRefShared(PyObject *op)
426426

427427
/* Tries to incref the object op and ensures that *src still points to it. */
428428
static inline int
429-
_Py_TryIncref(PyObject **src, PyObject *op)
429+
_Py_TryIncrefCompare(PyObject **src, PyObject *op)
430430
{
431431
if (_Py_TryIncrefFast(op)) {
432432
return 1;
@@ -452,7 +452,7 @@ _Py_XGetRef(PyObject **ptr)
452452
if (value == NULL) {
453453
return value;
454454
}
455-
if (_Py_TryIncref(ptr, value)) {
455+
if (_Py_TryIncrefCompare(ptr, value)) {
456456
return value;
457457
}
458458
}
@@ -467,7 +467,7 @@ _Py_TryXGetRef(PyObject **ptr)
467467
if (value == NULL) {
468468
return value;
469469
}
470-
if (_Py_TryIncref(ptr, value)) {
470+
if (_Py_TryIncrefCompare(ptr, value)) {
471471
return value;
472472
}
473473
return NULL;
@@ -506,8 +506,42 @@ _Py_XNewRefWithLock(PyObject *obj)
506506
return _Py_NewRefWithLock(obj);
507507
}
508508

509+
static inline void
510+
_PyObject_SetMaybeWeakref(PyObject *op)
511+
{
512+
if (_Py_IsImmortal(op)) {
513+
return;
514+
}
515+
for (;;) {
516+
Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&op->ob_ref_shared);
517+
if ((shared & _Py_REF_SHARED_FLAG_MASK) != 0) {
518+
// Nothing to do if it's in WEAKREFS, QUEUED, or MERGED states.
519+
return;
520+
}
521+
if (_Py_atomic_compare_exchange_ssize(
522+
&op->ob_ref_shared, &shared, shared | _Py_REF_MAYBE_WEAKREF)) {
523+
return;
524+
}
525+
}
526+
}
527+
509528
#endif
510529

530+
/* Tries to incref op and returns 1 if successful or 0 otherwise. */
531+
static inline int
532+
_Py_TryIncref(PyObject *op)
533+
{
534+
#ifdef Py_GIL_DISABLED
535+
return _Py_TryIncrefFast(op) || _Py_TryIncRefShared(op);
536+
#else
537+
if (Py_REFCNT(op) > 0) {
538+
Py_INCREF(op);
539+
return 1;
540+
}
541+
return 0;
542+
#endif
543+
}
544+
511545
#ifdef Py_REF_DEBUG
512546
extern void _PyInterpreterState_FinalizeRefTotal(PyInterpreterState *);
513547
extern void _Py_FinalizeRefTotal(_PyRuntimeState *);

Diff for: Include/internal/pycore_pyatomic_ft_wrappers.h

+5
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,23 @@ extern "C" {
2020
#endif
2121

2222
#ifdef Py_GIL_DISABLED
23+
#define FT_ATOMIC_LOAD_PTR(value) _Py_atomic_load_ptr(&value)
2324
#define FT_ATOMIC_LOAD_SSIZE(value) _Py_atomic_load_ssize(&value)
2425
#define FT_ATOMIC_LOAD_SSIZE_RELAXED(value) \
2526
_Py_atomic_load_ssize_relaxed(&value)
27+
#define FT_ATOMIC_STORE_PTR(value, new_value) \
28+
_Py_atomic_store_ptr(&value, new_value)
2629
#define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) \
2730
_Py_atomic_store_ptr_relaxed(&value, new_value)
2831
#define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) \
2932
_Py_atomic_store_ptr_release(&value, new_value)
3033
#define FT_ATOMIC_STORE_SSIZE_RELAXED(value, new_value) \
3134
_Py_atomic_store_ssize_relaxed(&value, new_value)
3235
#else
36+
#define FT_ATOMIC_LOAD_PTR(value) value
3337
#define FT_ATOMIC_LOAD_SSIZE(value) value
3438
#define FT_ATOMIC_LOAD_SSIZE_RELAXED(value) value
39+
#define FT_ATOMIC_STORE_PTR(value, new_value) value = new_value
3540
#define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) value = new_value
3641
#define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) value = new_value
3742
#define FT_ATOMIC_STORE_SSIZE_RELAXED(value, new_value) value = new_value

Diff for: Include/internal/pycore_weakref.h

+56-17
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,35 @@ extern "C" {
99
#endif
1010

1111
#include "pycore_critical_section.h" // Py_BEGIN_CRITICAL_SECTION()
12+
#include "pycore_lock.h"
1213
#include "pycore_object.h" // _Py_REF_IS_MERGED()
14+
#include "pycore_pyatomic_ft_wrappers.h"
15+
16+
#ifdef Py_GIL_DISABLED
17+
18+
#define WEAKREF_LIST_LOCK(obj) \
19+
_PyInterpreterState_GET() \
20+
->weakref_locks[((uintptr_t)obj) % NUM_WEAKREF_LIST_LOCKS]
21+
22+
// Lock using the referenced object
23+
#define LOCK_WEAKREFS(obj) \
24+
PyMutex_LockFlags(&WEAKREF_LIST_LOCK(obj), _Py_LOCK_DONT_DETACH)
25+
#define UNLOCK_WEAKREFS(obj) PyMutex_Unlock(&WEAKREF_LIST_LOCK(obj))
26+
27+
// Lock using a weakref
28+
#define LOCK_WEAKREFS_FOR_WR(wr) \
29+
PyMutex_LockFlags(wr->weakrefs_lock, _Py_LOCK_DONT_DETACH)
30+
#define UNLOCK_WEAKREFS_FOR_WR(wr) PyMutex_Unlock(wr->weakrefs_lock)
31+
32+
#else
33+
34+
#define LOCK_WEAKREFS(obj)
35+
#define UNLOCK_WEAKREFS(obj)
36+
37+
#define LOCK_WEAKREFS_FOR_WR(wr)
38+
#define UNLOCK_WEAKREFS_FOR_WR(wr)
39+
40+
#endif
1341

1442
static inline int _is_dead(PyObject *obj)
1543
{
@@ -30,53 +58,64 @@ static inline int _is_dead(PyObject *obj)
3058
static inline PyObject* _PyWeakref_GET_REF(PyObject *ref_obj)
3159
{
3260
assert(PyWeakref_Check(ref_obj));
33-
PyObject *ret = NULL;
34-
Py_BEGIN_CRITICAL_SECTION(ref_obj);
3561
PyWeakReference *ref = _Py_CAST(PyWeakReference*, ref_obj);
36-
PyObject *obj = ref->wr_object;
3762

63+
PyObject *obj = FT_ATOMIC_LOAD_PTR(ref->wr_object);
3864
if (obj == Py_None) {
3965
// clear_weakref() was called
40-
goto end;
66+
return NULL;
4167
}
4268

43-
if (_is_dead(obj)) {
44-
goto end;
69+
LOCK_WEAKREFS(obj);
70+
#ifdef Py_GIL_DISABLED
71+
if (ref->wr_object == Py_None) {
72+
// clear_weakref() was called
73+
UNLOCK_WEAKREFS(obj);
74+
return NULL;
4575
}
46-
#if !defined(Py_GIL_DISABLED)
47-
assert(Py_REFCNT(obj) > 0);
4876
#endif
49-
ret = Py_NewRef(obj);
50-
end:
51-
Py_END_CRITICAL_SECTION();
52-
return ret;
77+
if (_Py_TryIncref(obj)) {
78+
UNLOCK_WEAKREFS(obj);
79+
return obj;
80+
}
81+
UNLOCK_WEAKREFS(obj);
82+
return NULL;
5383
}
5484

5585
static inline int _PyWeakref_IS_DEAD(PyObject *ref_obj)
5686
{
5787
assert(PyWeakref_Check(ref_obj));
5888
int ret = 0;
59-
Py_BEGIN_CRITICAL_SECTION(ref_obj);
6089
PyWeakReference *ref = _Py_CAST(PyWeakReference*, ref_obj);
61-
PyObject *obj = ref->wr_object;
90+
PyObject *obj = FT_ATOMIC_LOAD_PTR(ref->wr_object);
6291
if (obj == Py_None) {
6392
// clear_weakref() was called
6493
ret = 1;
6594
}
6695
else {
96+
LOCK_WEAKREFS(obj);
6797
// See _PyWeakref_GET_REF() for the rationale of this test
98+
#ifdef Py_GIL_DISABLED
99+
ret = (ref->wr_object == Py_None) || _is_dead(obj);
100+
#else
68101
ret = _is_dead(obj);
102+
#endif
103+
UNLOCK_WEAKREFS(obj);
69104
}
70-
Py_END_CRITICAL_SECTION();
71105
return ret;
72106
}
73107

74-
extern Py_ssize_t _PyWeakref_GetWeakrefCount(PyWeakReference *head);
108+
extern Py_ssize_t _PyWeakref_GetWeakrefCount(PyObject *obj);
109+
110+
// Clear all the weak references to obj but leave their callbacks uncalled and
111+
// intact.
112+
extern void _PyWeakref_ClearWeakRefsExceptCallbacks(PyObject *obj);
75113

76114
extern void _PyWeakref_ClearRef(PyWeakReference *self);
77115

116+
PyAPI_FUNC(int) _PyWeakref_IsDead(PyObject *weakref);
117+
78118
#ifdef __cplusplus
79119
}
80120
#endif
81121
#endif /* !Py_INTERNAL_WEAKREF_H */
82-

Diff for: Lib/test/test_sys.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1708,11 +1708,15 @@ class newstyleclass(object): pass
17081708
# TODO: add check that forces layout of unicodefields
17091709
# weakref
17101710
import weakref
1711-
check(weakref.ref(int), size('2Pn3P'))
1711+
if support.Py_GIL_DISABLED:
1712+
expected = size('2Pn4P')
1713+
else:
1714+
expected = size('2Pn3P')
1715+
check(weakref.ref(int), expected)
17121716
# weakproxy
17131717
# XXX
17141718
# weakcallableproxy
1715-
check(weakref.proxy(int), size('2Pn3P'))
1719+
check(weakref.proxy(int), expected)
17161720

17171721
def check_slots(self, obj, base, extra):
17181722
expected = sys.getsizeof(base) + struct.calcsize(extra)

Diff for: Lib/test/test_weakref.py

+19
Original file line numberDiff line numberDiff line change
@@ -1907,6 +1907,25 @@ def test_threaded_weak_valued_consistency(self):
19071907
self.assertEqual(len(d), 1)
19081908
o = None # lose ref
19091909

1910+
@support.cpython_only
1911+
def test_weak_valued_consistency(self):
1912+
# A single-threaded, deterministic repro for issue #28427: old keys
1913+
# should not remove new values from WeakValueDictionary. This relies on
1914+
# an implementation detail of CPython's WeakValueDictionary (its
1915+
# underlying dictionary of KeyedRefs) to reproduce the issue.
1916+
d = weakref.WeakValueDictionary()
1917+
with support.disable_gc():
1918+
d[10] = RefCycle()
1919+
# Keep the KeyedRef alive after it's replaced so that GC will invoke
1920+
# the callback.
1921+
wr = d.data[10]
1922+
# Replace the value with something that isn't cyclic garbage
1923+
o = RefCycle()
1924+
d[10] = o
1925+
# Trigger GC, which will invoke the callback for `wr`
1926+
gc.collect()
1927+
self.assertEqual(len(d), 1)
1928+
19101929
def check_threaded_weak_dict_copy(self, type_, deepcopy):
19111930
# `type_` should be either WeakKeyDictionary or WeakValueDictionary.
19121931
# `deepcopy` should be either True or False.

Diff for: Modules/_sqlite/blob.c

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
#include "blob.h"
66
#include "util.h"
7-
#include "pycore_weakref.h" // _PyWeakref_GET_REF()
87

98
#define clinic_state() (pysqlite_get_state_by_type(Py_TYPE(self)))
109
#include "clinic/blob.c.h"
@@ -102,8 +101,8 @@ pysqlite_close_all_blobs(pysqlite_Connection *self)
102101
{
103102
for (int i = 0; i < PyList_GET_SIZE(self->blobs); i++) {
104103
PyObject *weakref = PyList_GET_ITEM(self->blobs, i);
105-
PyObject *blob = _PyWeakref_GET_REF(weakref);
106-
if (blob == NULL) {
104+
PyObject *blob;
105+
if (!PyWeakref_GetRef(weakref, &blob)) {
107106
continue;
108107
}
109108
close_blob((pysqlite_Blob *)blob);

Diff for: Modules/_sqlite/connection.c

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
#include "pycore_modsupport.h" // _PyArg_NoKeywords()
3939
#include "pycore_pyerrors.h" // _PyErr_ChainExceptions1()
4040
#include "pycore_pylifecycle.h" // _Py_IsInterpreterFinalizing()
41-
#include "pycore_weakref.h" // _PyWeakref_IS_DEAD()
41+
#include "pycore_weakref.h"
4242

4343
#include <stdbool.h>
4444

@@ -1065,7 +1065,7 @@ static void _pysqlite_drop_unused_cursor_references(pysqlite_Connection* self)
10651065

10661066
for (Py_ssize_t i = 0; i < PyList_Size(self->cursors); i++) {
10671067
PyObject* weakref = PyList_GetItem(self->cursors, i);
1068-
if (_PyWeakref_IS_DEAD(weakref)) {
1068+
if (_PyWeakref_IsDead(weakref)) {
10691069
continue;
10701070
}
10711071
if (PyList_Append(new_list, weakref) != 0) {

Diff for: Modules/_ssl.c

+6-7
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
#include "pycore_fileutils.h" // _PyIsSelectable_fd()
3030
#include "pycore_pyerrors.h" // _PyErr_ChainExceptions1()
3131
#include "pycore_time.h" // _PyDeadline_Init()
32-
#include "pycore_weakref.h" // _PyWeakref_GET_REF()
3332

3433
/* Include symbols from _socket module */
3534
#include "socketmodule.h"
@@ -392,8 +391,8 @@ typedef enum {
392391
// Return a borrowed reference.
393392
static inline PySocketSockObject* GET_SOCKET(PySSLSocket *obj) {
394393
if (obj->Socket) {
395-
PyObject *sock = _PyWeakref_GET_REF(obj->Socket);
396-
if (sock != NULL) {
394+
PyObject *sock;
395+
if (PyWeakref_GetRef(obj->Socket, &sock)) {
397396
// GET_SOCKET() returns a borrowed reference
398397
Py_DECREF(sock);
399398
}
@@ -2205,8 +2204,8 @@ PySSL_get_owner(PySSLSocket *self, void *c)
22052204
if (self->owner == NULL) {
22062205
Py_RETURN_NONE;
22072206
}
2208-
PyObject *owner = _PyWeakref_GET_REF(self->owner);
2209-
if (owner == NULL) {
2207+
PyObject *owner;
2208+
if (!PyWeakref_GetRef(self->owner, &owner)) {
22102209
Py_RETURN_NONE;
22112210
}
22122211
return owner;
@@ -4433,9 +4432,9 @@ _servername_callback(SSL *s, int *al, void *args)
44334432
* will be passed. If both do not exist only then the C-level object is
44344433
* passed. */
44354434
if (ssl->owner)
4436-
ssl_socket = _PyWeakref_GET_REF(ssl->owner);
4435+
PyWeakref_GetRef(ssl->owner, &ssl_socket);
44374436
else if (ssl->Socket)
4438-
ssl_socket = _PyWeakref_GET_REF(ssl->Socket);
4437+
PyWeakref_GetRef(ssl->Socket, &ssl_socket);
44394438
else
44404439
ssl_socket = Py_NewRef(ssl);
44414440

Diff for: Modules/_ssl/debughelpers.c

+3-3
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@ _PySSL_msg_callback(int write_p, int version, int content_type,
2828

2929
PyObject *ssl_socket; /* ssl.SSLSocket or ssl.SSLObject */
3030
if (ssl_obj->owner)
31-
ssl_socket = _PyWeakref_GET_REF(ssl_obj->owner);
31+
PyWeakref_GetRef(ssl_obj->owner, &ssl_socket);
3232
else if (ssl_obj->Socket)
33-
ssl_socket = _PyWeakref_GET_REF(ssl_obj->Socket);
33+
PyWeakref_GetRef(ssl_obj->Socket, &ssl_socket);
3434
else
3535
ssl_socket = (PyObject *)Py_NewRef(ssl_obj);
36-
assert(ssl_socket != NULL); // _PyWeakref_GET_REF() can return NULL
36+
assert(ssl_socket != NULL); // PyWeakref_GetRef() can return NULL
3737

3838
/* assume that OpenSSL verifies all payload and buf len is of sufficient
3939
length */

0 commit comments

Comments
 (0)