Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
0e995ad
wip: update type slots, stop-the-world
nascheme Mar 12, 2025
b173f17
Remove unneeded atomics for tp_flags.
nascheme Mar 13, 2025
d4ce112
Use stop-the-world for tp_flag changes too.
nascheme Mar 13, 2025
44eb332
Remove 'world_stops' and 'sys._get_world_stops'.
nascheme Mar 14, 2025
d132fab
Improve code comments.
nascheme Mar 14, 2025
658bcd5
Remove TSAN suppressions that seem unneeded.
nascheme Mar 14, 2025
ef2f07b
Add NEWS file.
nascheme Mar 14, 2025
ce8536d
Use mutex rather than critical sections.
nascheme Mar 22, 2025
b97a4b4
Merge 'origin/main' into gh-127266-type-slots-ts
nascheme Mar 26, 2025
ca00e74
Fix non-debug build.
nascheme Mar 26, 2025
6db4542
Improve comments.
nascheme Mar 26, 2025
398ac14
Merge 'origin/main' into gh-127266-type-slots-ts
nascheme Mar 27, 2025
75d6b71
Avoid unused function warning.
nascheme Mar 27, 2025
895a86a
Remove unwanted suppression (bad merge).
nascheme Mar 31, 2025
65e40f4
Fixes based on review feedback.
nascheme Mar 31, 2025
b68f1a1
Remove spurious assert().
nascheme Mar 31, 2025
9976b32
Merge 'origin/main' into gh-127266-type-slots-ts
nascheme Apr 1, 2025
1b84486
Omit mutex_tid member from default build.
nascheme Apr 1, 2025
2af9e49
Remove Py_TPFLAGS_EXPOSED flag and related logic.
nascheme Apr 1, 2025
f65e87c
Improve comment for TYPE_LOCK.
nascheme Apr 1, 2025
395a6d3
Re-add the ASSERT_NEW_OR_STOPPED() asserts.
nascheme Apr 1, 2025
e4f87e5
Further cleanups of the locking in typeobject.
nascheme Apr 3, 2025
2a66555
Fix data race in resolve_slotdups().
nascheme Apr 3, 2025
2efac26
Fix comment.
nascheme Apr 3, 2025
f7d2d36
Make the init of tp_dict thread-safe.
nascheme Apr 9, 2025
f3fd35a
Do some addtional locking simplification.
nascheme Apr 9, 2025
57c2a44
Avoid acquiring the types mutex if version is set.
nascheme Apr 10, 2025
7c0ccf5
Add check_invalid_reentrancy() call.
nascheme Apr 17, 2025
4fa77bb
Fix additional re-entrancy issues found.
nascheme Apr 17, 2025
caf6554
Add comment explaining class_name() code.
nascheme Apr 17, 2025
803d703
Merge 'origin/main' into gh-127266-type-slots-ts
nascheme Apr 17, 2025
90ea541
Use atomic load to avoid thread safety issue.
nascheme Apr 17, 2025
0dc0faf
Fix default debug build.
nascheme Apr 17, 2025
956e5d1
Move declaration to avoid syntax error.
nascheme Apr 17, 2025
2bb710c
Remove _PyType_GetVersionForCurrentState, unused.
nascheme Apr 21, 2025
3a9bc96
Fix for possible re-entrancy in has_custom_mro().
nascheme Apr 21, 2025
d742a53
Use correct FT_ATOMIC_ macro.
nascheme Apr 21, 2025
e7480c3
Remove TSAN suppression for 'assign_version_tag'.
nascheme Apr 21, 2025
e2ea281
Small efficiency fix for types_mutex_set_owned().
nascheme Apr 21, 2025
935bfca
Revert to using critical section with TYPE_LOCK.
nascheme Apr 21, 2025
1cff448
Merge 'origin/main' into gh-127266-type-slots-ts
nascheme Apr 21, 2025
a81e9e3
Invalidate type cache before calling watchers.
nascheme Apr 21, 2025
f5df0c3
Fixes for type_modified_unlocked().
nascheme Apr 22, 2025
7db281c
Major re-work, TYPE_LOCK protects more things.
nascheme Apr 22, 2025
da2a0ad
Merge 'origin/main' into gh-127266-type-slots-ts
nascheme Apr 23, 2025
986f23a
Fix non-debug build.
nascheme Apr 23, 2025
c404ed4
Revert unneeded code changes.
nascheme Apr 23, 2025
55af4ba
Merge branch 'origin/main' into gh-127266-type-slots-ts
nascheme Apr 23, 2025
0eb77da
Restore comment
nascheme Apr 24, 2025
16f15b2
Revert more changes.
nascheme Apr 24, 2025
0c328cc
Merge 'origin/main' into gh-127266-type-slots-ts
nascheme Apr 25, 2025
64547e9
Reduce item list size for a few tests.
nascheme Apr 25, 2025
fff1bd2
Merge 'origin/main' into gh-127266-type-slots-ts
nascheme Apr 28, 2025
5672352
Minor code tidy.
nascheme Apr 28, 2025
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
2 changes: 1 addition & 1 deletion Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ extern int _PyDict_CheckConsistency(PyObject *mp, int check_content);
// Fast inlined version of PyType_HasFeature()
static inline int
_PyType_HasFeature(PyTypeObject *type, unsigned long feature) {
return ((FT_ATOMIC_LOAD_ULONG_RELAXED(type->tp_flags) & feature) != 0);
return ((type->tp_flags) & feature) != 0;
}

extern void _PyType_InitCache(PyInterpreterState *interp);
Expand Down
6 changes: 1 addition & 5 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -776,11 +776,7 @@ PyType_HasFeature(PyTypeObject *type, unsigned long feature)
// PyTypeObject is opaque in the limited C API
flags = PyType_GetFlags(type);
#else
# ifdef Py_GIL_DISABLED
flags = _Py_atomic_load_ulong_relaxed(&type->tp_flags);
# else
flags = type->tp_flags;
# endif
flags = type->tp_flags;
#endif
return ((flags & feature) != 0);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
In the free-threaded build, avoid data races caused by updating type slots
or type flags after the type was initially created. For those (typically
rare) cases, use the stop-the-world mechanism. Remove the use of atomics
when reading or writing type flags. The use of atomics is not sufficient to
avoid races (since flags are sometimes read without a lock and without
atomics) and are no longer required.
85 changes: 78 additions & 7 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ class object "PyObject *" "&PyBaseObject_Type"

#endif

// Modification of type slots and type flags is protected by the global type
// lock. However, the slots and flags are read non-atomically without holding
// the type lock. So, we need to stop-the-world while modifying these, in
// order to avoid data races. This is unfortunately a bit expensive.
#ifdef Py_GIL_DISABLED
// If defined, type slot updates (after initial creation) will stop-the-world.
#define TYPE_SLOT_UPDATE_NEEDS_STOP
// If defined, type flag updates (after initial creation) will stop-the-world.
#define TYPE_FLAGS_UPDATE_NEEDS_STOP
#endif

#define PyTypeObject_CAST(op) ((PyTypeObject *)(op))

typedef struct PySlot_Offset {
Expand Down Expand Up @@ -353,9 +364,7 @@ type_set_flags(PyTypeObject *tp, unsigned long flags)
// held when flags are modified.
ASSERT_TYPE_LOCK_HELD();
}
// Since PyType_HasFeature() reads the flags without holding the type
// lock, we need an atomic store here.
FT_ATOMIC_STORE_ULONG_RELAXED(tp->tp_flags, flags);
tp->tp_flags = flags;
}

static void
Expand Down Expand Up @@ -1584,9 +1593,10 @@ type_set_abstractmethods(PyObject *tp, PyObject *value, void *Py_UNUSED(closure)
BEGIN_TYPE_LOCK();
type_modified_unlocked(type);
if (abstract)
type_add_flags(type, Py_TPFLAGS_IS_ABSTRACT);
_PyType_SetFlags(type, 0, Py_TPFLAGS_IS_ABSTRACT);
else
type_clear_flags(type, Py_TPFLAGS_IS_ABSTRACT);
_PyType_SetFlags(type, Py_TPFLAGS_IS_ABSTRACT, 0);

END_TYPE_LOCK();

return 0;
Expand Down Expand Up @@ -3490,6 +3500,9 @@ static int update_slot(PyTypeObject *, PyObject *);
static void fixup_slot_dispatchers(PyTypeObject *);
static int type_new_set_names(PyTypeObject *);
static int type_new_init_subclass(PyTypeObject *, PyObject *);
#ifdef Py_GIL_DISABLED
static bool has_slotdef(PyObject *);
#endif

/*
* Helpers for __dict__ descriptor. We don't want to expose the dicts
Expand Down Expand Up @@ -3687,7 +3700,7 @@ type_init(PyObject *cls, PyObject *args, PyObject *kwds)
unsigned long
PyType_GetFlags(PyTypeObject *type)
{
return FT_ATOMIC_LOAD_ULONG_RELAXED(type->tp_flags);
return type->tp_flags;
}


Expand Down Expand Up @@ -5779,7 +5792,17 @@ void
_PyType_SetFlags(PyTypeObject *self, unsigned long mask, unsigned long flags)
{
BEGIN_TYPE_LOCK();
type_set_flags_with_mask(self, mask, flags);
unsigned long new_flags = (self->tp_flags & ~mask) | flags;
if (new_flags != self->tp_flags) {
#ifdef TYPE_FLAGS_UPDATE_NEEDS_STOP
PyInterpreterState *interp = _PyInterpreterState_GET();
_PyEval_StopTheWorld(interp);
#endif
self->tp_flags = new_flags;
#ifdef TYPE_FLAGS_UPDATE_NEEDS_STOP
_PyEval_StartTheWorld(interp);
#endif
}
END_TYPE_LOCK();
}

Expand Down Expand Up @@ -5943,6 +5966,21 @@ _Py_type_getattro(PyObject *tp, PyObject *name)
return _Py_type_getattro_impl(type, name, NULL);
}

#ifdef TYPE_SLOT_UPDATE_NEEDS_STOP
static int
update_slot_world_stopped(PyTypeObject *type, PyObject *name)
{
int ret;
PyInterpreterState *interp = _PyInterpreterState_GET();
_PyEval_StopTheWorld(interp);
ret = update_slot(type, name);
_PyEval_StartTheWorld(interp);
return ret;
}
#endif

// Called by type_setattro(). Updates both the type dict and
// any type slots that correspond to the modified entry.
static int
type_update_dict(PyTypeObject *type, PyDictObject *dict, PyObject *name,
PyObject *value, PyObject **old_value)
Expand Down Expand Up @@ -5972,9 +6010,15 @@ type_update_dict(PyTypeObject *type, PyDictObject *dict, PyObject *name,
return -1;
}

#ifdef TYPE_SLOT_UPDATE_NEEDS_STOP
if (is_dunder_name(name) && has_slotdef(name)) {
return update_slot_world_stopped(type, name);
}
#else
if (is_dunder_name(name)) {
return update_slot(type, name);
}
#endif

return 0;
}
Expand Down Expand Up @@ -11002,6 +11046,20 @@ resolve_slotdups(PyTypeObject *type, PyObject *name)
#undef ptrs
}

#ifdef TYPE_SLOT_UPDATE_NEEDS_STOP
// Return true if "name" corresponds to at least one slot definition. This is
// a more accurate but more expensive test compared to is_dunder_name().
static bool
has_slotdef(PyObject *name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I built a temp dictionary on main interp init that can be used for this. In final version I didn't store it in interp state but maybe it is worth to store it (I have intermediate commits for this - bac95a5, b0ad875, 79a165d)
JFYI.

{
for (pytype_slotdef *p = slotdefs; p->name_strobj; p++) {
if (p->name_strobj == name) {
return true;
}
}
return false;
}
#endif

/* Common code for update_slots_callback() and fixup_slot_dispatchers().
*
Expand Down Expand Up @@ -11241,20 +11299,33 @@ fixup_slot_dispatchers(PyTypeObject *type)
END_TYPE_LOCK();
}

// Called when __bases__ is re-assigned.
static void
update_all_slots(PyTypeObject* type)
{
pytype_slotdef *p;

ASSERT_TYPE_LOCK_HELD();

#ifdef TYPE_SLOT_UPDATE_NEEDS_STOP
// Similar to update_slot_world_stopped(), this is required to avoid
// races. We don't use update_slot_world_stopped() here because we want
// to stop once rather than once per slot.
PyInterpreterState *interp = _PyInterpreterState_GET();
_PyEval_StopTheWorld(interp);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_PyEval_StopTheWorld() is always defined and is a no-op in the default build.

#endif

/* Clear the VALID_VERSION flag of 'type' and all its subclasses. */
type_modified_unlocked(type);

for (p = slotdefs; p->name; p++) {
/* update_slot returns int but can't actually fail */
update_slot(type, p->name_strobj);
}

#ifdef TYPE_SLOT_UPDATE_NEEDS_STOP
_PyEval_StartTheWorld(interp);
#endif
}


Expand Down
5 changes: 0 additions & 5 deletions Tools/tsan/suppressions_free_threading.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,13 @@

race_top:assign_version_tag
race_top:_multiprocessing_SemLock_acquire_impl
race_top:_Py_slot_tp_getattr_hook
race_top:dump_traceback
race_top:fatal_error
race_top:_multiprocessing_SemLock_release_impl
race_top:_PyFrame_GetCode
race_top:_PyFrame_Initialize
race_top:_PyObject_TryGetInstanceAttribute
race_top:PyUnstable_InterpreterFrame_GetLine
race_top:type_modified_unlocked
race_top:write_thread_id

# gh-129068: race on shared range iterators (test_free_threading.test_zip.ZipThreading.test_threading)
Expand All @@ -31,9 +29,6 @@ race_top:rangeiter_next
# gh-129748: test.test_free_threading.test_slots.TestSlots.test_object
race_top:mi_block_set_nextx

# gh-127266: type slot updates are not thread-safe (test_opcache.test_load_attr_method_lazy_dict)
race_top:update_one_slot

# https://gist.github.com/mpage/6962e8870606cfc960e159b407a0cb40
thread:pthread_create

Expand Down
Loading