Skip to content

gh-111926: Make weakrefs thread-safe in free-threaded builds #117168

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 67 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
9bf3820
Add _PyObject_SetMaybeWeakref
mpage Mar 14, 2024
75603c1
Make PyWeakref_NewRef thread-safe
mpage Mar 15, 2024
c4f8453
Make PyWeakref_NewProxy thread-safe
mpage Mar 15, 2024
ece230f
Make weakref___new___ thread-safe
mpage Mar 15, 2024
9acae59
Make weakref_hash thread-safe
mpage Mar 15, 2024
6d00f51
Make _PyStaticType_ClearWeakRefs thread-safe
mpage Mar 15, 2024
363a1a6
Handle multiple weakrefs
mpage Mar 15, 2024
c9b3bf9
Make _PyWeakref_GetWeakrefCount thread-safe
mpage Mar 16, 2024
b7b3315
Make _weakref._getweakrefs thread-safe
mpage Mar 16, 2024
e6e2d28
Make weakref clearing in subtype_dealloc thread-safe
mpage Mar 16, 2024
0d259d1
Fix incorrect size in test_sys for weakref/proxy
mpage Mar 16, 2024
7cc523b
Use a once flag
mpage Mar 18, 2024
11be247
Simplify clearing callback-free refs
mpage Mar 18, 2024
8fcd33f
Rationalize naming
mpage Mar 18, 2024
1e1e609
Fix warning
mpage Mar 18, 2024
19a274e
Simplify ClearWeakRefs
mpage Mar 19, 2024
817a5a3
Refactor clearing code
mpage Mar 19, 2024
d493e7d
Protect wr_object with a separate mutex
mpage Mar 20, 2024
88078a6
Switch to a sharded lock for linked list and object
mpage Mar 21, 2024
dc1f651
Fix formatting
mpage Mar 22, 2024
37c511a
Remove vestigial include
mpage Mar 22, 2024
ccaeded
Make sure we set the cleared flag
mpage Mar 22, 2024
6a50381
Use `Py_TryIncref` when returning the ref
mpage Mar 23, 2024
d998d1b
Don't leak ref if initialization fails
mpage Mar 24, 2024
37076b5
Remove clinic critical section directives for `_weakref` module
mpage Mar 24, 2024
a07d2db
Support async removal of dead weakrefs from WeakValueDictionary
mpage Mar 24, 2024
a901848
Only use the striped lock
mpage Mar 26, 2024
8c85fd3
Refactor weakref creation
mpage Mar 27, 2024
0021bc5
Fixups
mpage Mar 27, 2024
40d99d4
Fix formatting
mpage Mar 27, 2024
dd922b1
Use QSBR to avoid locking in PyType_IsSubtype
mpage Mar 26, 2024
681872e
Remove unnecessary include
mpage Mar 27, 2024
8982831
Fix unused function warning in default builds
mpage Mar 27, 2024
334c766
Check for callable proxy
mpage Mar 28, 2024
0549445
Refactor weakref creation
mpage Mar 29, 2024
eb12c06
Simplify comment
mpage Mar 29, 2024
700cf4c
Get rid of GetWeakrefCountThreadsafe
mpage Mar 29, 2024
63200cd
Don't explicitly zero initialize striped lock
mpage Mar 29, 2024
e9eed72
Indent conditional define
mpage Mar 29, 2024
e3d5779
Use non-atomic loads while we hold the lock
mpage Mar 29, 2024
f31f1e5
Refactor a _PyWeakref_GET_REF and _PyWeakref_IS_DEAD to share more code
mpage Mar 29, 2024
b5c9561
Merge helper functions for clearing weakrefs
mpage Mar 29, 2024
93425e6
Make _PyStaticType_ClearWeakRefs have one implementation for default …
mpage Mar 29, 2024
89edf02
Make sure we're able to index into the striped lock even if GC clears us
mpage Mar 29, 2024
7f73480
Consolidate implementations of _PyWeakref_ClearRef and gc_clear
mpage Mar 29, 2024
d742ed1
Rename `_Py_TryIncref` to `_Py_TryIncrefCompare`
mpage Mar 29, 2024
37f9ceb
Add `_Py_TryIncref`
mpage Mar 29, 2024
fb9eee5
Refactor `weakref._getweakrefs` to have one implementation
mpage Mar 29, 2024
d760b99
Refactor to have one implementation for `PyObject_ClearWeakRefs`
mpage Mar 29, 2024
491e4ff
Revert "Use QSBR to avoid locking in PyType_IsSubtype"
mpage Mar 29, 2024
531a076
Accept thread unsafety when checking subtype for now
mpage Mar 29, 2024
ed280d8
Update modules to use PyWeakref_GetRef
mpage Mar 29, 2024
2750787
Merge branch 'main' into gh-111926-thread-safe-weakref
mpage Mar 29, 2024
5e7c8bb
Use a pointer to the appropriate mutex rather than a copy of the obje…
mpage Mar 29, 2024
37c9529
Fix formatting
mpage Mar 29, 2024
fece88d
Merge implementation of try_reuse_basic_ref
mpage Mar 29, 2024
7a86e35
Remove redundant gc untrack
mpage Apr 2, 2024
f89edd1
Update comment
mpage Apr 2, 2024
5a0b976
Add missing braces
mpage Apr 2, 2024
9e67740
Use Py_ssize_t for tracking number of items
mpage Apr 2, 2024
55a211f
Tighten up clearing basic refs/proxies
mpage Apr 2, 2024
cdb9290
Simplify _PyStaticType_ClearWeakRefs
mpage Apr 2, 2024
c93c336
Simplify get_or_create_weakref
mpage Apr 2, 2024
0366f01
Add _PyWeakref_IsDead
mpage Apr 2, 2024
3e135e4
Remove TODO
mpage Apr 2, 2024
dbf7c58
Fix formatting
mpage Apr 2, 2024
73466bf
Update Python/pystate.c
colesbury Apr 8, 2024
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
8 changes: 8 additions & 0 deletions Include/cpython/weakrefobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ struct _PyWeakReference {
PyWeakReference *wr_prev;
PyWeakReference *wr_next;
vectorcallfunc vectorcall;

#ifdef Py_GIL_DISABLED
/* Pointer to the lock used when clearing in free-threaded builds.
* Normally this can be derived from wr_object, but in some cases we need
* to lock after wr_object has been set to Py_None.
*/
struct _PyMutex *weakrefs_lock;
#endif
};

Py_DEPRECATED(3.13) static inline PyObject* PyWeakref_GET_OBJECT(PyObject *ref_obj)
Expand Down
7 changes: 7 additions & 0 deletions Include/internal/pycore_interp.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ struct _stoptheworld_state {
PyThreadState *requester; // Thread that requested the pause (may be NULL).
};

#ifdef Py_GIL_DISABLED
// This should be prime but otherwise the choice is arbitrary. A larger value
// increases concurrency at the expense of memory.
# define NUM_WEAKREF_LIST_LOCKS 127
#endif

/* cross-interpreter data registry */

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

// Per-interpreter state for the obmalloc allocator. For the main
Expand Down
40 changes: 37 additions & 3 deletions Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ _Py_TryIncRefShared(PyObject *op)

/* Tries to incref the object op and ensures that *src still points to it. */
static inline int
_Py_TryIncref(PyObject **src, PyObject *op)
_Py_TryIncrefCompare(PyObject **src, PyObject *op)
{
if (_Py_TryIncrefFast(op)) {
return 1;
Expand All @@ -453,7 +453,7 @@ _Py_XGetRef(PyObject **ptr)
if (value == NULL) {
return value;
}
if (_Py_TryIncref(ptr, value)) {
if (_Py_TryIncrefCompare(ptr, value)) {
return value;
}
}
Expand All @@ -468,7 +468,7 @@ _Py_TryXGetRef(PyObject **ptr)
if (value == NULL) {
return value;
}
if (_Py_TryIncref(ptr, value)) {
if (_Py_TryIncrefCompare(ptr, value)) {
return value;
}
return NULL;
Expand Down Expand Up @@ -507,8 +507,42 @@ _Py_XNewRefWithLock(PyObject *obj)
return _Py_NewRefWithLock(obj);
}

static inline void
_PyObject_SetMaybeWeakref(PyObject *op)
{
if (_Py_IsImmortal(op)) {
return;
}
for (;;) {
Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&op->ob_ref_shared);
if ((shared & _Py_REF_SHARED_FLAG_MASK) != 0) {
// Nothing to do if it's in WEAKREFS, QUEUED, or MERGED states.
return;
}
if (_Py_atomic_compare_exchange_ssize(
&op->ob_ref_shared, &shared, shared | _Py_REF_MAYBE_WEAKREF)) {
return;
}
}
}

#endif

/* Tries to incref op and returns 1 if successful or 0 otherwise. */
static inline int
_Py_TryIncref(PyObject *op)
{
#ifdef Py_GIL_DISABLED
return _Py_TryIncrefFast(op) || _Py_TryIncRefShared(op);
#else
if (Py_REFCNT(op) > 0) {
Py_INCREF(op);
return 1;
}
return 0;
#endif
}

#ifdef Py_REF_DEBUG
extern void _PyInterpreterState_FinalizeRefTotal(PyInterpreterState *);
extern void _Py_FinalizeRefTotal(_PyRuntimeState *);
Expand Down
5 changes: 5 additions & 0 deletions Include/internal/pycore_pyatomic_ft_wrappers.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,23 @@ extern "C" {
#endif

#ifdef Py_GIL_DISABLED
#define FT_ATOMIC_LOAD_PTR(value) _Py_atomic_load_ptr(&value)
#define FT_ATOMIC_LOAD_SSIZE(value) _Py_atomic_load_ssize(&value)
#define FT_ATOMIC_LOAD_SSIZE_RELAXED(value) \
_Py_atomic_load_ssize_relaxed(&value)
#define FT_ATOMIC_STORE_PTR(value, new_value) \
_Py_atomic_store_ptr(&value, new_value)
#define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) \
_Py_atomic_store_ptr_relaxed(&value, new_value)
#define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) \
_Py_atomic_store_ptr_release(&value, new_value)
#define FT_ATOMIC_STORE_SSIZE_RELAXED(value, new_value) \
_Py_atomic_store_ssize_relaxed(&value, new_value)
#else
#define FT_ATOMIC_LOAD_PTR(value) value
#define FT_ATOMIC_LOAD_SSIZE(value) value
#define FT_ATOMIC_LOAD_SSIZE_RELAXED(value) value
#define FT_ATOMIC_STORE_PTR(value, new_value) value = new_value
#define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) value = new_value
#define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) value = new_value
#define FT_ATOMIC_STORE_SSIZE_RELAXED(value, new_value) value = new_value
Expand Down
73 changes: 56 additions & 17 deletions Include/internal/pycore_weakref.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,35 @@ extern "C" {
#endif

#include "pycore_critical_section.h" // Py_BEGIN_CRITICAL_SECTION()
#include "pycore_lock.h"
#include "pycore_object.h" // _Py_REF_IS_MERGED()
#include "pycore_pyatomic_ft_wrappers.h"

#ifdef Py_GIL_DISABLED

#define WEAKREF_LIST_LOCK(obj) \
_PyInterpreterState_GET() \
->weakref_locks[((uintptr_t)obj) % NUM_WEAKREF_LIST_LOCKS]

// Lock using the referenced object
#define LOCK_WEAKREFS(obj) \
PyMutex_LockFlags(&WEAKREF_LIST_LOCK(obj), _Py_LOCK_DONT_DETACH)
#define UNLOCK_WEAKREFS(obj) PyMutex_Unlock(&WEAKREF_LIST_LOCK(obj))

// Lock using a weakref
#define LOCK_WEAKREFS_FOR_WR(wr) \
PyMutex_LockFlags(wr->weakrefs_lock, _Py_LOCK_DONT_DETACH)
#define UNLOCK_WEAKREFS_FOR_WR(wr) PyMutex_Unlock(wr->weakrefs_lock)

#else

#define LOCK_WEAKREFS(obj)
#define UNLOCK_WEAKREFS(obj)

#define LOCK_WEAKREFS_FOR_WR(wr)
#define UNLOCK_WEAKREFS_FOR_WR(wr)

#endif

static inline int _is_dead(PyObject *obj)
{
Expand All @@ -30,53 +58,64 @@ static inline int _is_dead(PyObject *obj)
static inline PyObject* _PyWeakref_GET_REF(PyObject *ref_obj)
{
assert(PyWeakref_Check(ref_obj));
PyObject *ret = NULL;
Py_BEGIN_CRITICAL_SECTION(ref_obj);
PyWeakReference *ref = _Py_CAST(PyWeakReference*, ref_obj);
PyObject *obj = ref->wr_object;

PyObject *obj = FT_ATOMIC_LOAD_PTR(ref->wr_object);
if (obj == Py_None) {
// clear_weakref() was called
goto end;
return NULL;
}

if (_is_dead(obj)) {
goto end;
LOCK_WEAKREFS(obj);
#ifdef Py_GIL_DISABLED
if (ref->wr_object == Py_None) {
// clear_weakref() was called
UNLOCK_WEAKREFS(obj);
return NULL;
}
#if !defined(Py_GIL_DISABLED)
assert(Py_REFCNT(obj) > 0);
#endif
ret = Py_NewRef(obj);
end:
Py_END_CRITICAL_SECTION();
return ret;
if (_Py_TryIncref(obj)) {
UNLOCK_WEAKREFS(obj);
return obj;
}
UNLOCK_WEAKREFS(obj);
return NULL;
}

static inline int _PyWeakref_IS_DEAD(PyObject *ref_obj)
{
assert(PyWeakref_Check(ref_obj));
int ret = 0;
Py_BEGIN_CRITICAL_SECTION(ref_obj);
PyWeakReference *ref = _Py_CAST(PyWeakReference*, ref_obj);
PyObject *obj = ref->wr_object;
PyObject *obj = FT_ATOMIC_LOAD_PTR(ref->wr_object);
if (obj == Py_None) {
// clear_weakref() was called
ret = 1;
}
else {
LOCK_WEAKREFS(obj);
// See _PyWeakref_GET_REF() for the rationale of this test
#ifdef Py_GIL_DISABLED
ret = (ref->wr_object == Py_None) || _is_dead(obj);
#else
ret = _is_dead(obj);
#endif
UNLOCK_WEAKREFS(obj);
}
Py_END_CRITICAL_SECTION();
return ret;
}

extern Py_ssize_t _PyWeakref_GetWeakrefCount(PyWeakReference *head);
extern Py_ssize_t _PyWeakref_GetWeakrefCount(PyObject *obj);

// Clear all the weak references to obj but leave their callbacks uncalled and
// intact.
extern void _PyWeakref_ClearWeakRefsExceptCallbacks(PyObject *obj);

extern void _PyWeakref_ClearRef(PyWeakReference *self);

PyAPI_FUNC(int) _PyWeakref_IsDead(PyObject *weakref);

#ifdef __cplusplus
}
#endif
#endif /* !Py_INTERNAL_WEAKREF_H */

8 changes: 6 additions & 2 deletions Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1708,11 +1708,15 @@ class newstyleclass(object): pass
# TODO: add check that forces layout of unicodefields
# weakref
import weakref
check(weakref.ref(int), size('2Pn3P'))
if support.Py_GIL_DISABLED:
expected = size('2Pn4P')
else:
expected = size('2Pn3P')
check(weakref.ref(int), expected)
# weakproxy
# XXX
# weakcallableproxy
check(weakref.proxy(int), size('2Pn3P'))
check(weakref.proxy(int), expected)

def check_slots(self, obj, base, extra):
expected = sys.getsizeof(base) + struct.calcsize(extra)
Expand Down
19 changes: 19 additions & 0 deletions Lib/test/test_weakref.py
Original file line number Diff line number Diff line change
Expand Up @@ -1865,6 +1865,25 @@ def test_threaded_weak_valued_consistency(self):
self.assertEqual(len(d), 1)
o = None # lose ref

@support.cpython_only
def test_weak_valued_consistency(self):
# A single-threaded, deterministic repro for issue #28427: old keys
# should not remove new values from WeakValueDictionary. This relies on
# an implementation detail of CPython's WeakValueDictionary (its
# underlying dictionary of KeyedRefs) to reproduce the issue.
d = weakref.WeakValueDictionary()
with support.disable_gc():
d[10] = RefCycle()
# Keep the KeyedRef alive after it's replaced so that GC will invoke
# the callback.
wr = d.data[10]
# Replace the value with something that isn't cyclic garbage
o = RefCycle()
d[10] = o
# Trigger GC, which will invoke the callback for `wr`
gc.collect()
self.assertEqual(len(d), 1)

def check_threaded_weak_dict_copy(self, type_, deepcopy):
# `type_` should be either WeakKeyDictionary or WeakValueDictionary.
# `deepcopy` should be either True or False.
Expand Down
5 changes: 2 additions & 3 deletions Modules/_sqlite/blob.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

#include "blob.h"
#include "util.h"
#include "pycore_weakref.h" // _PyWeakref_GET_REF()

#define clinic_state() (pysqlite_get_state_by_type(Py_TYPE(self)))
#include "clinic/blob.c.h"
Expand Down Expand Up @@ -102,8 +101,8 @@ pysqlite_close_all_blobs(pysqlite_Connection *self)
{
for (int i = 0; i < PyList_GET_SIZE(self->blobs); i++) {
PyObject *weakref = PyList_GET_ITEM(self->blobs, i);
PyObject *blob = _PyWeakref_GET_REF(weakref);
if (blob == NULL) {
PyObject *blob;
if (!PyWeakref_GetRef(weakref, &blob)) {
continue;
}
close_blob((pysqlite_Blob *)blob);
Expand Down
4 changes: 2 additions & 2 deletions Modules/_sqlite/connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
#include "pycore_modsupport.h" // _PyArg_NoKeywords()
#include "pycore_pyerrors.h" // _PyErr_ChainExceptions1()
#include "pycore_pylifecycle.h" // _Py_IsInterpreterFinalizing()
#include "pycore_weakref.h" // _PyWeakref_IS_DEAD()
#include "pycore_weakref.h"

#include <stdbool.h>

Expand Down Expand Up @@ -1065,7 +1065,7 @@ static void _pysqlite_drop_unused_cursor_references(pysqlite_Connection* self)

for (Py_ssize_t i = 0; i < PyList_Size(self->cursors); i++) {
PyObject* weakref = PyList_GetItem(self->cursors, i);
if (_PyWeakref_IS_DEAD(weakref)) {
if (_PyWeakref_IsDead(weakref)) {
continue;
}
if (PyList_Append(new_list, weakref) != 0) {
Expand Down
13 changes: 6 additions & 7 deletions Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
#include "pycore_fileutils.h" // _PyIsSelectable_fd()
#include "pycore_pyerrors.h" // _PyErr_ChainExceptions1()
#include "pycore_time.h" // _PyDeadline_Init()
#include "pycore_weakref.h" // _PyWeakref_GET_REF()

/* Include symbols from _socket module */
#include "socketmodule.h"
Expand Down Expand Up @@ -392,8 +391,8 @@ typedef enum {
// Return a borrowed reference.
static inline PySocketSockObject* GET_SOCKET(PySSLSocket *obj) {
if (obj->Socket) {
PyObject *sock = _PyWeakref_GET_REF(obj->Socket);
if (sock != NULL) {
PyObject *sock;
if (PyWeakref_GetRef(obj->Socket, &sock)) {
// GET_SOCKET() returns a borrowed reference
Py_DECREF(sock);
}
Expand Down Expand Up @@ -2205,8 +2204,8 @@ PySSL_get_owner(PySSLSocket *self, void *c)
if (self->owner == NULL) {
Py_RETURN_NONE;
}
PyObject *owner = _PyWeakref_GET_REF(self->owner);
if (owner == NULL) {
PyObject *owner;
if (!PyWeakref_GetRef(self->owner, &owner)) {
Py_RETURN_NONE;
}
return owner;
Expand Down Expand Up @@ -4433,9 +4432,9 @@ _servername_callback(SSL *s, int *al, void *args)
* will be passed. If both do not exist only then the C-level object is
* passed. */
if (ssl->owner)
ssl_socket = _PyWeakref_GET_REF(ssl->owner);
PyWeakref_GetRef(ssl->owner, &ssl_socket);
else if (ssl->Socket)
ssl_socket = _PyWeakref_GET_REF(ssl->Socket);
PyWeakref_GetRef(ssl->Socket, &ssl_socket);
else
ssl_socket = Py_NewRef(ssl);

Expand Down
6 changes: 3 additions & 3 deletions Modules/_ssl/debughelpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@ _PySSL_msg_callback(int write_p, int version, int content_type,

PyObject *ssl_socket; /* ssl.SSLSocket or ssl.SSLObject */
if (ssl_obj->owner)
ssl_socket = _PyWeakref_GET_REF(ssl_obj->owner);
PyWeakref_GetRef(ssl_obj->owner, &ssl_socket);
else if (ssl_obj->Socket)
ssl_socket = _PyWeakref_GET_REF(ssl_obj->Socket);
PyWeakref_GetRef(ssl_obj->Socket, &ssl_socket);
else
ssl_socket = (PyObject *)Py_NewRef(ssl_obj);
assert(ssl_socket != NULL); // _PyWeakref_GET_REF() can return NULL
assert(ssl_socket != NULL); // PyWeakref_GetRef() can return NULL

/* assume that OpenSSL verifies all payload and buf len is of sufficient
length */
Expand Down
Loading
Loading