Skip to content

Commit 0dddcb6

Browse files
committed
weakref: make weakrefs thread-safe without the GIL
Also reduce GC frequency in "collect_in_thread" to improve test_weakref speed when running without the GIL.
1 parent 967fe31 commit 0dddcb6

20 files changed

+575
-569
lines changed

Doc/data/stable_abi.dat

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/cpython/weakrefobject.h

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,55 +2,59 @@
22
# error "this header file must not be included directly"
33
#endif
44

5-
/* PyWeakReference is the base struct for the Python ReferenceType, ProxyType,
6-
* and CallableProxyType.
7-
*/
8-
struct _PyWeakReference {
5+
struct _PyWeakrefBase {
96
PyObject_HEAD
107

8+
/* If wr_object is weakly referenced, wr_object has a doubly-linked NULL-
9+
* terminated list of weak references to it. These are the list pointers.
10+
* If wr_object goes away, wr_object is set to Py_None, and these pointers
11+
* have no meaning then.
12+
*/
13+
struct _PyWeakrefBase *wr_prev;
14+
struct _PyWeakrefBase *wr_next;
15+
};
16+
17+
struct _PyWeakrefControl {
18+
struct _PyWeakrefBase base;
19+
20+
/* Protectes the weakref linked-list and wr_object from
21+
* concurrent accesses. */
22+
_PyMutex mutex;
23+
1124
/* The object to which this is a weak reference, or Py_None if none.
1225
* Note that this is a stealth reference: wr_object's refcount is
1326
* not incremented to reflect this pointer.
1427
*/
1528
PyObject *wr_object;
29+
};
30+
31+
/* PyWeakReference is the base struct for the Python ReferenceType, ProxyType,
32+
* and CallableProxyType.
33+
*/
34+
struct _PyWeakReference {
35+
struct _PyWeakrefBase base;
36+
37+
/* Pointer to weakref control block */
38+
struct _PyWeakrefControl *wr_parent;
1639

1740
/* A callable to invoke when wr_object dies, or NULL if none. */
1841
PyObject *wr_callback;
1942

43+
vectorcallfunc vectorcall;
44+
2045
/* A cache for wr_object's hash code. As usual for hashes, this is -1
2146
* if the hash code isn't known yet.
2247
*/
2348
Py_hash_t hash;
24-
25-
/* If wr_object is weakly referenced, wr_object has a doubly-linked NULL-
26-
* terminated list of weak references to it. These are the list pointers.
27-
* If wr_object goes away, wr_object is set to Py_None, and these pointers
28-
* have no meaning then.
29-
*/
30-
PyWeakReference *wr_prev;
31-
PyWeakReference *wr_next;
32-
vectorcallfunc vectorcall;
3349
};
3450

35-
PyAPI_FUNC(Py_ssize_t) _PyWeakref_GetWeakrefCount(PyWeakReference *head);
51+
typedef struct _PyWeakrefControl PyWeakrefControl;
52+
typedef struct _PyWeakrefBase PyWeakrefBase;
53+
54+
PyAPI_FUNC(void) _PyWeakref_DetachRef(PyWeakReference *self);
55+
56+
PyAPI_FUNC(Py_ssize_t) _PyWeakref_GetWeakrefCount(struct _PyWeakrefControl *ctrl);
3657

3758
PyAPI_FUNC(void) _PyWeakref_ClearRef(PyWeakReference *self);
3859

39-
static inline PyObject* PyWeakref_GET_OBJECT(PyObject *ref_obj) {
40-
PyWeakReference *ref;
41-
PyObject *obj;
42-
assert(PyWeakref_Check(ref_obj));
43-
ref = _Py_CAST(PyWeakReference*, ref_obj);
44-
obj = ref->wr_object;
45-
// Explanation for the Py_REFCNT() check: when a weakref's target is part
46-
// of a long chain of deallocations which triggers the trashcan mechanism,
47-
// clearing the weakrefs can be delayed long after the target's refcount
48-
// has dropped to zero. In the meantime, code accessing the weakref will
49-
// be able to "see" the target object even though it is supposed to be
50-
// unreachable. See issue gh-60806.
51-
if (Py_IS_REFERENCED(obj)) {
52-
return obj;
53-
}
54-
return Py_None;
55-
}
56-
#define PyWeakref_GET_OBJECT(ref) PyWeakref_GET_OBJECT(_PyObject_CAST(ref))
60+
#define PyWeakref_GET_OBJECT(ref) PyWeakref_GetObject(_PyObject_CAST(ref))

Include/internal/pycore_object.h

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -380,21 +380,21 @@ extern void _Py_PrintReferenceAddresses(FILE *);
380380
* nor should it be used to add, remove, or swap any refs in the list.
381381
* That is the sole responsibility of the code in weakrefobject.c.
382382
*/
383-
static inline PyObject **
384-
_PyObject_GET_WEAKREFS_LISTPTR(PyObject *op)
383+
static inline PyWeakrefControl **
384+
_PyObject_GET_WEAKREFS_CONTROLPTR(PyObject *op)
385385
{
386386
if (PyType_Check(op) &&
387387
((PyTypeObject *)op)->tp_flags & _Py_TPFLAGS_STATIC_BUILTIN) {
388388
static_builtin_state *state = _PyStaticType_GetState(
389389
(PyTypeObject *)op);
390390
return _PyStaticType_GET_WEAKREFS_LISTPTR(state);
391391
}
392-
// Essentially _PyObject_GET_WEAKREFS_LISTPTR_FROM_OFFSET():
392+
// Essentially _PyObject_GET_WEAKREFS_CONTROLPTR_FROM_OFFSET():
393393
Py_ssize_t offset = Py_TYPE(op)->tp_weaklistoffset;
394-
return (PyObject **)((char *)op + offset);
394+
return (PyWeakrefControl **)((char *)op + offset);
395395
}
396396

397-
/* This is a special case of _PyObject_GET_WEAKREFS_LISTPTR().
397+
/* This is a special case of _PyObject_GET_WEAKREFS_CONTROLPTR().
398398
* Only the most fundamental lookup path is used.
399399
* Consequently, static types should not be used.
400400
*
@@ -404,15 +404,21 @@ _PyObject_GET_WEAKREFS_LISTPTR(PyObject *op)
404404
* are only finalized at the end of runtime finalization.
405405
*
406406
* If the weaklist for static types is actually needed then use
407-
* _PyObject_GET_WEAKREFS_LISTPTR().
407+
* _PyObject_GET_WEAKREFS_CONTROLPTR().
408408
*/
409-
static inline PyWeakReference **
410-
_PyObject_GET_WEAKREFS_LISTPTR_FROM_OFFSET(PyObject *op)
409+
static inline PyWeakrefControl **
410+
_PyObject_GET_WEAKREFS_CONTROLPTR_FROM_OFFSET(PyObject *op)
411411
{
412412
assert(!PyType_Check(op) ||
413413
((PyTypeObject *)op)->tp_flags & Py_TPFLAGS_HEAPTYPE);
414414
Py_ssize_t offset = Py_TYPE(op)->tp_weaklistoffset;
415-
return (PyWeakReference **)((char *)op + offset);
415+
return (PyWeakrefControl **)((char *)op + offset);
416+
}
417+
418+
static inline PyWeakrefControl *
419+
_PyObject_GET_WEAKREF_CONTROL(PyObject *op)
420+
{
421+
return _Py_atomic_load_ptr(_PyObject_GET_WEAKREFS_CONTROLPTR(op));
416422
}
417423

418424

Include/internal/pycore_typeobject.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ typedef struct {
5252
they will effectively never get triggered. However, there
5353
are also some diagnostic uses for the list of weakrefs,
5454
so we still keep it. */
55-
PyObject *tp_weaklist;
55+
PyWeakrefControl *tp_weaklist;
5656
} static_builtin_state;
5757

58-
static inline PyObject **
58+
static inline PyWeakrefControl **
5959
_PyStaticType_GET_WEAKREFS_LISTPTR(static_builtin_state *state)
6060
{
6161
assert(state != NULL);

Include/object.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ PyAPI_FUNC(int) PyObject_IsTrue(PyObject *);
347347
PyAPI_FUNC(int) PyObject_Not(PyObject *);
348348
PyAPI_FUNC(int) PyCallable_Check(PyObject *);
349349
PyAPI_FUNC(void) PyObject_ClearWeakRefs(PyObject *);
350+
#ifndef Py_LIMITED_API
351+
PyAPI_FUNC(void) _PyObject_ClearWeakRefsFromDealloc(PyObject *);
352+
#endif
350353

351354
/* PyObject_Dir(obj) acts like Python builtins.dir(obj), returning a
352355
list of strings. PyObject_Dir(NULL) is like builtins.dir(),

Include/weakrefobject.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,18 @@ PyAPI_FUNC(PyObject *) PyWeakref_NewRef(PyObject *ob,
2828
PyAPI_FUNC(PyObject *) PyWeakref_NewProxy(PyObject *ob,
2929
PyObject *callback);
3030
PyAPI_FUNC(PyObject *) PyWeakref_GetObject(PyObject *ref);
31-
31+
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x030B0000
32+
PyAPI_FUNC(PyObject *) PyWeakref_FetchObject(PyObject *ref);
33+
#endif
34+
#define PyWeakref_LockObject PyWeakref_FetchObject
3235

3336
#ifndef Py_LIMITED_API
3437
# define Py_CPYTHON_WEAKREFOBJECT_H
3538
# include "cpython/weakrefobject.h"
3639
# undef Py_CPYTHON_WEAKREFOBJECT_H
3740
#endif
3841

42+
3943
#ifdef __cplusplus
4044
}
4145
#endif

Lib/test/test_capi/test_misc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,7 @@ def test_heaptype_with_weakref(self):
634634
inst = _testcapi.HeapCTypeWithWeakref()
635635
ref = weakref.ref(inst)
636636
self.assertEqual(ref(), inst)
637-
self.assertEqual(inst.weakreflist, ref)
637+
self.assertEqual(type(inst.weakreflist).__name__, "weakref_control")
638638

639639
def test_heaptype_with_managed_weakref(self):
640640
inst = _testcapi.HeapCTypeWithManagedWeakref()

Lib/test/test_stable_abi_ctypes.py

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_sys.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,11 +1582,11 @@ class newstyleclass(object): pass
15821582
# TODO: add check that forces layout of unicodefields
15831583
# weakref
15841584
import weakref
1585-
check(weakref.ref(int), size('2Pn3P'))
1585+
check(weakref.ref(int), size('4Pn'))
15861586
# weakproxy
15871587
# XXX
15881588
# weakcallableproxy
1589-
check(weakref.proxy(int), size('2Pn3P'))
1589+
check(weakref.proxy(int), size('4Pn'))
15901590

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

Lib/test/test_weakref.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def callback(self, ref):
7979

8080

8181
@contextlib.contextmanager
82-
def collect_in_thread(period=0.0001):
82+
def collect_in_thread(period=0.005):
8383
"""
8484
Ensure GC collections happen in a different thread, at a high frequency.
8585
"""
@@ -990,9 +990,9 @@ class MyRef(weakref.ref):
990990
self.assertEqual(weakref.getweakrefcount(o), 3)
991991
refs = weakref.getweakrefs(o)
992992
self.assertEqual(len(refs), 3)
993-
self.assertIs(r2, refs[0])
994-
self.assertIn(r1, refs[1:])
995-
self.assertIn(r3, refs[1:])
993+
self.assertIn(r2, refs)
994+
self.assertIn(r1, refs)
995+
self.assertIn(r3, refs)
996996

997997
def test_subclass_refs_dont_conflate_callbacks(self):
998998
class MyRef(weakref.ref):

0 commit comments

Comments
 (0)