Skip to content

Commit a3d657a

Browse files
pythongh-120198: Stop the world when setting __class__ on free-threaded build (pythonGH-120672)
1 parent 867cf40 commit a3d657a

File tree

5 files changed

+68
-57
lines changed

5 files changed

+68
-57
lines changed

Include/internal/pycore_dict.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,8 @@ _PyInlineValuesSize(PyTypeObject *tp)
323323
int
324324
_PyDict_DetachFromObject(PyDictObject *dict, PyObject *obj);
325325

326+
PyDictObject *_PyObject_MaterializeManagedDict_LockHeld(PyObject *);
327+
326328
#ifdef __cplusplus
327329
}
328330
#endif

Include/object.h

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -311,9 +311,9 @@ static inline Py_ssize_t Py_REFCNT(PyObject *ob) {
311311
#if !defined(Py_GIL_DISABLED)
312312
return ob->ob_refcnt;
313313
#else
314-
uint32_t local = _Py_atomic_load_uint32_relaxed(&ob->ob_ref_local);
315-
if (local == _Py_IMMORTAL_REFCNT_LOCAL) {
316-
return _Py_IMMORTAL_REFCNT;
314+
static inline PyTypeObject* _Py_TYPE(PyObject *ob)
315+
{
316+
return ob->ob_type;
317317
}
318318
Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&ob->ob_ref_shared);
319319
return _Py_STATIC_CAST(Py_ssize_t, local) +
@@ -421,11 +421,7 @@ static inline void Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) {
421421

422422

423423
static inline void Py_SET_TYPE(PyObject *ob, PyTypeObject *type) {
424-
#ifdef Py_GIL_DISABLED
425-
_Py_atomic_store_ptr(&ob->ob_type, type);
426-
#else
427424
ob->ob_type = type;
428-
#endif
429425
}
430426
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000
431427
# define Py_SET_TYPE(ob, type) Py_SET_TYPE(_PyObject_CAST(ob), type)

Lib/test/test_free_threading/test_type.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ class Bar:
106106
thing = Foo()
107107
def work():
108108
foo = thing
109-
for _ in range(10000):
109+
for _ in range(5000):
110110
foo.__class__ = Bar
111111
type(foo)
112112
foo.__class__ = Foo

Objects/dictobject.c

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ ASSERT_DICT_LOCKED(PyObject *op)
158158
if (!_PyInterpreterState_GET()->stoptheworld.world_stopped) { \
159159
ASSERT_DICT_LOCKED(op); \
160160
}
161+
#define ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(op) \
162+
if (!_PyInterpreterState_GET()->stoptheworld.world_stopped) { \
163+
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op); \
164+
}
161165

162166
#define IS_DICT_SHARED(mp) _PyObject_GC_IS_SHARED(mp)
163167
#define SET_DICT_SHARED(mp) _PyObject_GC_SET_SHARED(mp)
@@ -227,6 +231,7 @@ static inline void split_keys_entry_added(PyDictKeysObject *keys)
227231

228232
#define ASSERT_DICT_LOCKED(op)
229233
#define ASSERT_WORLD_STOPPED_OR_DICT_LOCKED(op)
234+
#define ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(op)
230235
#define LOCK_KEYS(keys)
231236
#define UNLOCK_KEYS(keys)
232237
#define ASSERT_KEYS_LOCKED(keys)
@@ -6667,10 +6672,10 @@ make_dict_from_instance_attributes(PyInterpreterState *interp,
66676672
return res;
66686673
}
66696674

6670-
static PyDictObject *
6671-
materialize_managed_dict_lock_held(PyObject *obj)
6675+
PyDictObject *
6676+
_PyObject_MaterializeManagedDict_LockHeld(PyObject *obj)
66726677
{
6673-
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(obj);
6678+
ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(obj);
66746679

66756680
PyDictValues *values = _PyObject_InlineValues(obj);
66766681
PyInterpreterState *interp = _PyInterpreterState_GET();
@@ -6699,7 +6704,7 @@ _PyObject_MaterializeManagedDict(PyObject *obj)
66996704
goto exit;
67006705
}
67016706
#endif
6702-
dict = materialize_managed_dict_lock_held(obj);
6707+
dict = _PyObject_MaterializeManagedDict_LockHeld(obj);
67036708

67046709
#ifdef Py_GIL_DISABLED
67056710
exit:
@@ -7132,7 +7137,7 @@ PyObject_ClearManagedDict(PyObject *obj)
71327137
int
71337138
_PyDict_DetachFromObject(PyDictObject *mp, PyObject *obj)
71347139
{
7135-
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(obj);
7140+
ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(obj);
71367141
assert(_PyObject_ManagedDictPointer(obj)->dict == mp);
71377142
assert(_PyObject_InlineValuesConsistencyCheck(obj));
71387143

Objects/typeobject.c

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6369,28 +6369,11 @@ compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char*
63696369
return 0;
63706370
}
63716371

6372-
static int
6373-
object_set_class(PyObject *self, PyObject *value, void *closure)
6374-
{
6375-
6376-
if (value == NULL) {
6377-
PyErr_SetString(PyExc_TypeError,
6378-
"can't delete __class__ attribute");
6379-
return -1;
6380-
}
6381-
if (!PyType_Check(value)) {
6382-
PyErr_Format(PyExc_TypeError,
6383-
"__class__ must be set to a class, not '%s' object",
6384-
Py_TYPE(value)->tp_name);
6385-
return -1;
6386-
}
6387-
PyTypeObject *newto = (PyTypeObject *)value;
63886372

6389-
if (PySys_Audit("object.__setattr__", "OsO",
6390-
self, "__class__", value) < 0) {
6391-
return -1;
6392-
}
63936373

6374+
static int
6375+
object_set_class_world_stopped(PyObject *self, PyTypeObject *newto)
6376+
{
63946377
PyTypeObject *oldto = Py_TYPE(self);
63956378

63966379
/* In versions of CPython prior to 3.5, the code in
@@ -6456,49 +6439,74 @@ object_set_class(PyObject *self, PyObject *value, void *closure)
64566439
/* Changing the class will change the implicit dict keys,
64576440
* so we must materialize the dictionary first. */
64586441
if (oldto->tp_flags & Py_TPFLAGS_INLINE_VALUES) {
6459-
PyDictObject *dict = _PyObject_MaterializeManagedDict(self);
6442+
PyDictObject *dict = _PyObject_GetManagedDict(self);
64606443
if (dict == NULL) {
6461-
return -1;
6444+
dict = _PyObject_MaterializeManagedDict_LockHeld(self);
6445+
if (dict == NULL) {
6446+
return -1;
6447+
}
64626448
}
64636449

6464-
bool error = false;
6465-
6466-
Py_BEGIN_CRITICAL_SECTION2(self, dict);
6467-
6468-
// If we raced after materialization and replaced the dict
6469-
// then the materialized dict should no longer have the
6470-
// inline values in which case detach is a nop.
6471-
assert(_PyObject_GetManagedDict(self) == dict ||
6472-
dict->ma_values != _PyObject_InlineValues(self));
6450+
assert(_PyObject_GetManagedDict(self) == dict);
64736451

64746452
if (_PyDict_DetachFromObject(dict, self) < 0) {
6475-
error = true;
6476-
}
6477-
6478-
Py_END_CRITICAL_SECTION2();
6479-
if (error) {
64806453
return -1;
64816454
}
6455+
64826456
}
64836457
if (newto->tp_flags & Py_TPFLAGS_HEAPTYPE) {
64846458
Py_INCREF(newto);
64856459
}
6486-
Py_BEGIN_CRITICAL_SECTION(self);
6487-
// The real Py_TYPE(self) (`oldto`) may have changed from
6488-
// underneath us in another thread, so we re-fetch it here.
6489-
oldto = Py_TYPE(self);
6460+
64906461
Py_SET_TYPE(self, newto);
6491-
Py_END_CRITICAL_SECTION();
6462+
6463+
return 0;
6464+
}
6465+
else {
6466+
return -1;
6467+
}
6468+
}
6469+
6470+
static int
6471+
object_set_class(PyObject *self, PyObject *value, void *closure)
6472+
{
6473+
6474+
if (value == NULL) {
6475+
PyErr_SetString(PyExc_TypeError,
6476+
"can't delete __class__ attribute");
6477+
return -1;
6478+
}
6479+
if (!PyType_Check(value)) {
6480+
PyErr_Format(PyExc_TypeError,
6481+
"__class__ must be set to a class, not '%s' object",
6482+
Py_TYPE(value)->tp_name);
6483+
return -1;
6484+
}
6485+
PyTypeObject *newto = (PyTypeObject *)value;
6486+
6487+
if (PySys_Audit("object.__setattr__", "OsO",
6488+
self, "__class__", value) < 0) {
6489+
return -1;
6490+
}
6491+
6492+
#ifdef Py_GIL_DISABLED
6493+
PyInterpreterState *interp = _PyInterpreterState_GET();
6494+
_PyEval_StopTheWorld(interp);
6495+
#endif
6496+
PyTypeObject *oldto = Py_TYPE(self);
6497+
int res = object_set_class_world_stopped(self, newto);
6498+
#ifdef Py_GIL_DISABLED
6499+
_PyEval_StartTheWorld(interp);
6500+
#endif
6501+
if (res == 0) {
64926502
if (oldto->tp_flags & Py_TPFLAGS_HEAPTYPE) {
64936503
Py_DECREF(oldto);
64946504
}
64956505

64966506
RARE_EVENT_INC(set_class);
64976507
return 0;
64986508
}
6499-
else {
6500-
return -1;
6501-
}
6509+
return res;
65026510
}
65036511

65046512
static PyGetSetDef object_getsets[] = {

0 commit comments

Comments
 (0)