From 2e5667e60c8e736a048bad03b1c1b1556ab7b826 Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 18 Jun 2024 17:35:30 +0800 Subject: [PATCH 1/7] Stop the world when setting __class__ --- Include/object.h | 6 +----- Objects/dictobject.c | 6 +++++- Objects/typeobject.c | 39 +++++++++++++++++++++++---------------- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/Include/object.h b/Include/object.h index 48f97502ad19f1..9860688b72dea4 100644 --- a/Include/object.h +++ b/Include/object.h @@ -247,7 +247,7 @@ _Py_IsOwnedByCurrentThread(PyObject *ob) // bpo-39573: The Py_SET_TYPE() function must be used to set an object type. static inline PyTypeObject* Py_TYPE(PyObject *ob) { #ifdef Py_GIL_DISABLED - return (PyTypeObject *)_Py_atomic_load_ptr_relaxed(&ob->ob_type); + return (PyTypeObject *)&ob->ob_type; #else return ob->ob_type; #endif @@ -278,11 +278,7 @@ static inline int Py_IS_TYPE(PyObject *ob, PyTypeObject *type) { static inline void Py_SET_TYPE(PyObject *ob, PyTypeObject *type) { -#ifdef Py_GIL_DISABLED - _Py_atomic_store_ptr(&ob->ob_type, type); -#else ob->ob_type = type; -#endif } #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 # define Py_SET_TYPE(ob, type) Py_SET_TYPE(_PyObject_CAST(ob), type) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 32799bf5210fc3..79f305335ec73b 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -158,6 +158,10 @@ ASSERT_DICT_LOCKED(PyObject *op) if (!_PyInterpreterState_GET()->stoptheworld.world_stopped) { \ ASSERT_DICT_LOCKED(op); \ } +#define ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(op) \ + if (!_PyInterpreterState_GET()->stoptheworld.world_stopped) { \ + _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(op); \ + } #define IS_DICT_SHARED(mp) _PyObject_GC_IS_SHARED(mp) #define SET_DICT_SHARED(mp) _PyObject_GC_SET_SHARED(mp) @@ -7170,7 +7174,7 @@ PyObject_ClearManagedDict(PyObject *obj) int _PyDict_DetachFromObject(PyDictObject *mp, PyObject *obj) { - _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(obj); + ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(obj); assert(_PyObject_ManagedDictPointer(obj)->dict == mp); assert(_PyObject_InlineValuesConsistencyCheck(obj)); diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 958f42430c80a2..d786c9993a592a 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6599,6 +6599,12 @@ object_set_class(PyObject *self, PyObject *value, void *closure) } PyTypeObject *oldto = Py_TYPE(self); +#ifdef Py_GIL_DISABLED + PyInterpreterState *interp = PyInterpreterState_Get(); + // The real Py_TYPE(self) (`oldto`) may have changed from + // underneath us in another thread, so we re-fetch it here. + _PyEval_StopTheWorld(interp); +#endif /* In versions of CPython prior to 3.5, the code in compatible_for_assignment was not set up to correctly check for memory @@ -6656,7 +6662,7 @@ object_set_class(PyObject *self, PyObject *value, void *closure) PyErr_Format(PyExc_TypeError, "__class__ assignment only supported for mutable types " "or ModuleType subclasses"); - return -1; + goto err; } if (compatible_for_assignment(oldto, newto, "__class__")) { @@ -6665,47 +6671,48 @@ object_set_class(PyObject *self, PyObject *value, void *closure) if (oldto->tp_flags & Py_TPFLAGS_INLINE_VALUES) { PyDictObject *dict = _PyObject_MaterializeManagedDict(self); if (dict == NULL) { - return -1; + goto err; } - bool error = false; - - Py_BEGIN_CRITICAL_SECTION2(self, dict); - // If we raced after materialization and replaced the dict // then the materialized dict should no longer have the // inline values in which case detach is a nop. + // Note: we don't need to lock here because the world should be stopped. + assert(_PyObject_GetManagedDict(self) == dict || dict->ma_values != _PyObject_InlineValues(self)); if (_PyDict_DetachFromObject(dict, self) < 0) { - error = true; + goto err; } - Py_END_CRITICAL_SECTION2(); - if (error) { - return -1; - } } if (newto->tp_flags & Py_TPFLAGS_HEAPTYPE) { Py_INCREF(newto); } - Py_BEGIN_CRITICAL_SECTION(self); - // The real Py_TYPE(self) (`oldto`) may have changed from - // underneath us in another thread, so we re-fetch it here. + oldto = Py_TYPE(self); Py_SET_TYPE(self, newto); - Py_END_CRITICAL_SECTION(); + if (oldto->tp_flags & Py_TPFLAGS_HEAPTYPE) { Py_DECREF(oldto); } RARE_EVENT_INC(set_class); + +#ifdef Py_GIL_DISABLED + _PyEval_StartTheWorld(interp); +#endif return 0; } else { - return -1; + goto err; } +err: +#ifdef Py_GIL_DISABLED + _PyEval_StartTheWorld(interp); +#endif + return -1; } static PyGetSetDef object_getsets[] = { From b482f87274d8ae10e25f0ea764445b0dad936466 Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 18 Jun 2024 20:51:17 +0800 Subject: [PATCH 2/7] Address reviews --- Include/object.h | 4 ---- Objects/typeobject.c | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Include/object.h b/Include/object.h index 9860688b72dea4..1f8f52f53c288e 100644 --- a/Include/object.h +++ b/Include/object.h @@ -246,11 +246,7 @@ _Py_IsOwnedByCurrentThread(PyObject *ob) // bpo-39573: The Py_SET_TYPE() function must be used to set an object type. static inline PyTypeObject* Py_TYPE(PyObject *ob) { -#ifdef Py_GIL_DISABLED - return (PyTypeObject *)&ob->ob_type; -#else return ob->ob_type; -#endif } #if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 < 0x030b0000 # define Py_TYPE(ob) Py_TYPE(_PyObject_CAST(ob)) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index d786c9993a592a..7fe8c3a1d6be8c 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6600,7 +6600,7 @@ object_set_class(PyObject *self, PyObject *value, void *closure) PyTypeObject *oldto = Py_TYPE(self); #ifdef Py_GIL_DISABLED - PyInterpreterState *interp = PyInterpreterState_Get(); + PyInterpreterState *interp = _PyInterpreterState_GET(); // The real Py_TYPE(self) (`oldto`) may have changed from // underneath us in another thread, so we re-fetch it here. _PyEval_StopTheWorld(interp); From d2a7095b61d0ff350c3fc941572ed2649483ea1e Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:26:15 +0800 Subject: [PATCH 3/7] fix non-free-threaded builds --- Objects/dictobject.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 79f305335ec73b..1559acb3b284ea 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -231,6 +231,7 @@ static inline void split_keys_entry_added(PyDictKeysObject *keys) #define ASSERT_DICT_LOCKED(op) #define ASSERT_WORLD_STOPPED_OR_DICT_LOCKED(op) +#define ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(op) #define LOCK_KEYS(keys) #define UNLOCK_KEYS(keys) #define ASSERT_KEYS_LOCKED(keys) From 8a562ceafdff061e3e5caeef53324f26f8fd75e5 Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 19 Jun 2024 13:05:55 +0800 Subject: [PATCH 4/7] Address review --- Include/internal/pycore_dict.h | 2 ++ Objects/dictobject.c | 8 ++++---- Objects/typeobject.c | 18 ++++++++++-------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h index cfe837b1e2b3ab..3ea01398b5e207 100644 --- a/Include/internal/pycore_dict.h +++ b/Include/internal/pycore_dict.h @@ -323,6 +323,8 @@ _PyInlineValuesSize(PyTypeObject *tp) int _PyDict_DetachFromObject(PyDictObject *dict, PyObject *obj); +PyDictObject *_PyObject_materialize_managed_dict_lock_held(PyObject *); + #ifdef __cplusplus } #endif diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 1559acb3b284ea..0ae99a54cb9196 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -6708,10 +6708,10 @@ make_dict_from_instance_attributes(PyInterpreterState *interp, return res; } -static PyDictObject * -materialize_managed_dict_lock_held(PyObject *obj) +PyDictObject * +_PyObject_materialize_managed_dict_lock_held(PyObject *obj) { - _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(obj); + ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(obj); PyDictValues *values = _PyObject_InlineValues(obj); PyInterpreterState *interp = _PyInterpreterState_GET(); @@ -6740,7 +6740,7 @@ _PyObject_MaterializeManagedDict(PyObject *obj) goto exit; } #endif - dict = materialize_managed_dict_lock_held(obj); + dict = _PyObject_materialize_managed_dict_lock_held(obj); #ifdef Py_GIL_DISABLED exit: diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 7fe8c3a1d6be8c..1b4bb1f7c9d4a6 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6598,13 +6598,13 @@ object_set_class(PyObject *self, PyObject *value, void *closure) return -1; } - PyTypeObject *oldto = Py_TYPE(self); #ifdef Py_GIL_DISABLED PyInterpreterState *interp = _PyInterpreterState_GET(); // The real Py_TYPE(self) (`oldto`) may have changed from - // underneath us in another thread, so we re-fetch it here. + // underneath us in another thread, so we stop the world. _PyEval_StopTheWorld(interp); #endif + PyTypeObject *oldto = Py_TYPE(self); /* In versions of CPython prior to 3.5, the code in compatible_for_assignment was not set up to correctly check for memory @@ -6669,9 +6669,12 @@ object_set_class(PyObject *self, PyObject *value, void *closure) /* Changing the class will change the implicit dict keys, * so we must materialize the dictionary first. */ if (oldto->tp_flags & Py_TPFLAGS_INLINE_VALUES) { - PyDictObject *dict = _PyObject_MaterializeManagedDict(self); + PyDictObject *dict = _PyObject_GetManagedDict(self); if (dict == NULL) { - goto err; + dict = _PyObject_materialize_managed_dict_lock_held(self); + if (dict == NULL) { + goto err; + } } // If we raced after materialization and replaced the dict @@ -6691,7 +6694,9 @@ object_set_class(PyObject *self, PyObject *value, void *closure) Py_INCREF(newto); } - oldto = Py_TYPE(self); +#ifdef Py_GIL_DISABLED + _PyEval_StartTheWorld(interp); +#endif Py_SET_TYPE(self, newto); if (oldto->tp_flags & Py_TPFLAGS_HEAPTYPE) { @@ -6700,9 +6705,6 @@ object_set_class(PyObject *self, PyObject *value, void *closure) RARE_EVENT_INC(set_class); -#ifdef Py_GIL_DISABLED - _PyEval_StartTheWorld(interp); -#endif return 0; } else { From 8a25bfb79a9348bc7fd5796ad0eb4ddd16620862 Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 21 Jun 2024 18:38:16 +0800 Subject: [PATCH 5/7] Address Sam's reviews --- Include/internal/pycore_dict.h | 2 +- Objects/dictobject.c | 4 +- Objects/typeobject.c | 91 +++++++++++++++++++--------------- 3 files changed, 53 insertions(+), 44 deletions(-) diff --git a/Include/internal/pycore_dict.h b/Include/internal/pycore_dict.h index 3ea01398b5e207..950b5d61920fe6 100644 --- a/Include/internal/pycore_dict.h +++ b/Include/internal/pycore_dict.h @@ -323,7 +323,7 @@ _PyInlineValuesSize(PyTypeObject *tp) int _PyDict_DetachFromObject(PyDictObject *dict, PyObject *obj); -PyDictObject *_PyObject_materialize_managed_dict_lock_held(PyObject *); +PyDictObject *_PyObject_MaterializeManagedDict_LockHeld(PyObject *); #ifdef __cplusplus } diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 0ae99a54cb9196..115cd213e6ba16 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -6709,7 +6709,7 @@ make_dict_from_instance_attributes(PyInterpreterState *interp, } PyDictObject * -_PyObject_materialize_managed_dict_lock_held(PyObject *obj) +_PyObject_MaterializeManagedDict_LockHeld(PyObject *obj) { ASSERT_WORLD_STOPPED_OR_OBJ_LOCKED(obj); @@ -6740,7 +6740,7 @@ _PyObject_MaterializeManagedDict(PyObject *obj) goto exit; } #endif - dict = _PyObject_materialize_managed_dict_lock_held(obj); + dict = _PyObject_MaterializeManagedDict_LockHeld(obj); #ifdef Py_GIL_DISABLED exit: diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 1deed2feac5ff4..34a6c21d5e2c78 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6604,34 +6604,11 @@ compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char* return 0; } -static int -object_set_class(PyObject *self, PyObject *value, void *closure) -{ - - if (value == NULL) { - PyErr_SetString(PyExc_TypeError, - "can't delete __class__ attribute"); - return -1; - } - if (!PyType_Check(value)) { - PyErr_Format(PyExc_TypeError, - "__class__ must be set to a class, not '%s' object", - Py_TYPE(value)->tp_name); - return -1; - } - PyTypeObject *newto = (PyTypeObject *)value; - if (PySys_Audit("object.__setattr__", "OsO", - self, "__class__", value) < 0) { - return -1; - } -#ifdef Py_GIL_DISABLED - PyInterpreterState *interp = _PyInterpreterState_GET(); - // The real Py_TYPE(self) (`oldto`) may have changed from - // underneath us in another thread, so we stop the world. - _PyEval_StopTheWorld(interp); -#endif +static int +object_set_class_no_world_stopped(PyObject *self, PyTypeObject *newto, PyTypeObject **oldto_p) +{ PyTypeObject *oldto = Py_TYPE(self); /* In versions of CPython prior to 3.5, the code in @@ -6690,7 +6667,7 @@ object_set_class(PyObject *self, PyObject *value, void *closure) PyErr_Format(PyExc_TypeError, "__class__ assignment only supported for mutable types " "or ModuleType subclasses"); - goto err; + return -1; } if (compatible_for_assignment(oldto, newto, "__class__")) { @@ -6699,9 +6676,9 @@ object_set_class(PyObject *self, PyObject *value, void *closure) if (oldto->tp_flags & Py_TPFLAGS_INLINE_VALUES) { PyDictObject *dict = _PyObject_GetManagedDict(self); if (dict == NULL) { - dict = _PyObject_materialize_managed_dict_lock_held(self); + dict = _PyObject_MaterializeManagedDict_LockHeld(self); if (dict == NULL) { - goto err; + return -1; } } @@ -6714,7 +6691,7 @@ object_set_class(PyObject *self, PyObject *value, void *closure) dict->ma_values != _PyObject_InlineValues(self)); if (_PyDict_DetachFromObject(dict, self) < 0) { - goto err; + return -1; } } @@ -6722,27 +6699,59 @@ object_set_class(PyObject *self, PyObject *value, void *closure) Py_INCREF(newto); } -#ifdef Py_GIL_DISABLED - _PyEval_StartTheWorld(interp); -#endif Py_SET_TYPE(self, newto); - if (oldto->tp_flags & Py_TPFLAGS_HEAPTYPE) { - Py_DECREF(oldto); - } - - RARE_EVENT_INC(set_class); + *oldto_p = oldto; return 0; } else { - goto err; + return -1; } -err: +} + +static int +object_set_class(PyObject *self, PyObject *value, void *closure) +{ + + if (value == NULL) { + PyErr_SetString(PyExc_TypeError, + "can't delete __class__ attribute"); + return -1; + } + if (!PyType_Check(value)) { + PyErr_Format(PyExc_TypeError, + "__class__ must be set to a class, not '%s' object", + Py_TYPE(value)->tp_name); + return -1; + } + PyTypeObject *newto = (PyTypeObject *)value; + + if (PySys_Audit("object.__setattr__", "OsO", + self, "__class__", value) < 0) { + return -1; + } + +#ifdef Py_GIL_DISABLED + PyInterpreterState *interp = _PyInterpreterState_GET(); + // The real Py_TYPE(self) (`oldto`) may have changed from + // underneath us in another thread, so we stop the world. + _PyEval_StopTheWorld(interp); +#endif + PyTypeObject *oldto; + int res = object_set_class_no_world_stopped(self, newto, &oldto); #ifdef Py_GIL_DISABLED _PyEval_StartTheWorld(interp); #endif - return -1; + if (res == 0) { + if (oldto->tp_flags & Py_TPFLAGS_HEAPTYPE) { + Py_DECREF(oldto); + } + + RARE_EVENT_INC(set_class); + return 0; + } + return res; } static PyGetSetDef object_getsets[] = { From 13486cbd15bdef396169f63aeaaf044db6aa9d9c Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Fri, 21 Jun 2024 20:43:10 +0800 Subject: [PATCH 6/7] fix name --- Objects/typeobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 34a6c21d5e2c78..34ea813b99c07f 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6607,7 +6607,7 @@ compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char* static int -object_set_class_no_world_stopped(PyObject *self, PyTypeObject *newto, PyTypeObject **oldto_p) +object_set_class_world_stopped(PyObject *self, PyTypeObject *newto, PyTypeObject **oldto_p) { PyTypeObject *oldto = Py_TYPE(self); @@ -6739,7 +6739,7 @@ object_set_class(PyObject *self, PyObject *value, void *closure) _PyEval_StopTheWorld(interp); #endif PyTypeObject *oldto; - int res = object_set_class_no_world_stopped(self, newto, &oldto); + int res = object_set_class_world_stopped(self, newto, &oldto); #ifdef Py_GIL_DISABLED _PyEval_StartTheWorld(interp); #endif From a409ab27ca0a2deab743d76fb7a7074e2fb791c1 Mon Sep 17 00:00:00 2001 From: Ken Jin <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 10 Jul 2024 23:56:57 +0800 Subject: [PATCH 7/7] Address reviews --- Lib/test/test_free_threading/test_type.py | 2 +- Objects/typeobject.c | 18 ++++-------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_free_threading/test_type.py b/Lib/test/test_free_threading/test_type.py index 75259795e81bcb..649676db9c08a5 100644 --- a/Lib/test/test_free_threading/test_type.py +++ b/Lib/test/test_free_threading/test_type.py @@ -106,7 +106,7 @@ class Bar: thing = Foo() def work(): foo = thing - for _ in range(10000): + for _ in range(5000): foo.__class__ = Bar type(foo) foo.__class__ = Foo diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 34ea813b99c07f..4450bbfc274aef 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -6607,7 +6607,7 @@ compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char* static int -object_set_class_world_stopped(PyObject *self, PyTypeObject *newto, PyTypeObject **oldto_p) +object_set_class_world_stopped(PyObject *self, PyTypeObject *newto) { PyTypeObject *oldto = Py_TYPE(self); @@ -6682,13 +6682,7 @@ object_set_class_world_stopped(PyObject *self, PyTypeObject *newto, PyTypeObject } } - // If we raced after materialization and replaced the dict - // then the materialized dict should no longer have the - // inline values in which case detach is a nop. - // Note: we don't need to lock here because the world should be stopped. - - assert(_PyObject_GetManagedDict(self) == dict || - dict->ma_values != _PyObject_InlineValues(self)); + assert(_PyObject_GetManagedDict(self) == dict); if (_PyDict_DetachFromObject(dict, self) < 0) { return -1; @@ -6701,8 +6695,6 @@ object_set_class_world_stopped(PyObject *self, PyTypeObject *newto, PyTypeObject Py_SET_TYPE(self, newto); - *oldto_p = oldto; - return 0; } else { @@ -6734,12 +6726,10 @@ object_set_class(PyObject *self, PyObject *value, void *closure) #ifdef Py_GIL_DISABLED PyInterpreterState *interp = _PyInterpreterState_GET(); - // The real Py_TYPE(self) (`oldto`) may have changed from - // underneath us in another thread, so we stop the world. _PyEval_StopTheWorld(interp); #endif - PyTypeObject *oldto; - int res = object_set_class_world_stopped(self, newto, &oldto); + PyTypeObject *oldto = Py_TYPE(self); + int res = object_set_class_world_stopped(self, newto); #ifdef Py_GIL_DISABLED _PyEval_StartTheWorld(interp); #endif