Skip to content

Commit 8770238

Browse files
committed
Do type slot and __bases__ updates in a single stop-the-world.
Remove the publish generation counter and the retry loops. Instead of preparing updates under TYPE_LOCK and publishing them with the world stopped (and rebuilding them if they went stale while the lock was released), do the work inside the stop-the-world: - Slot updates after type_setattro() of a slot name and after __bases__ assignment look up the descriptors and store the slots directly with the world stopped. The slot update queue is removed. The lookups use borrowed references and skip dict keys that are not exact str (_PyDict_GetItem_ExactUnicode()), so no user code runs and nothing is deallocated. - __bases__ assignment validates the new bases (inheritance cycle, best base, layout compatibility) and stores tp_bases and tp_base in the same stop-the-world. The checks return an error code and the exception is raised after the world is started. PyType_Ready() of a new base and the Py_TPFLAGS_BASETYPE check move to type_check_new_bases(). ht_slots are compared by string content. In the free-threaded build, slot computation for new types (fixup_slot_dispatchers() and overrides_hash()) also skips non-str keys, using the thread-safe _PyDict_GetItemStackRef_ExactUnicode(), so that slots do not depend on when they were computed.
1 parent 04afc4d commit 8770238

5 files changed

Lines changed: 452 additions & 457 deletions

File tree

‎Include/internal/pycore_dict.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,10 @@ PyAPI_FUNC(int) _PyDict_SetItem_KnownHash_LockHeld(PyDictObject *mp, PyObject *k
170170
PyAPI_FUNC(int) _PyDict_GetItemRef_KnownHash_LockHeld(PyDictObject *op, PyObject *key, Py_hash_t hash, PyObject **result);
171171
PyAPI_FUNC(int) _PyDict_GetItemRef_KnownHash(PyDictObject *op, PyObject *key, Py_hash_t hash, PyObject **result);
172172
extern int _PyDict_GetItemRef_Unicode_LockHeld(PyDictObject *op, PyObject *key, PyObject **result);
173+
extern PyObject *_PyDict_GetItem_ExactUnicode(PyDictObject *mp, PyObject *key);
174+
extern int _PyDict_GetItemStackRef_ExactUnicode(PyDictObject *mp,
175+
PyObject *key,
176+
_PyStackRef *value_addr);
173177
PyAPI_FUNC(int) _PyObjectDict_SetItem(PyTypeObject *tp, PyObject *obj, PyObject **dictptr, PyObject *name, PyObject *value);
174178

175179
extern int _PyDict_Pop_KnownHash(

‎Include/internal/pycore_interp_structs.h‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -588,12 +588,6 @@ struct types_state {
588588
where all those lower numbers are used for core static types. */
589589
unsigned int next_version_tag;
590590

591-
#ifdef Py_GIL_DISABLED
592-
/* Incremented on changes that can invalidate a type update that is
593-
prepared under the type lock and published with the world stopped. */
594-
uint64_t publish_gen;
595-
#endif
596-
597591
/* Every static builtin type is initialized for each interpreter
598592
during its own initialization, including for the main interpreter
599593
during global runtime initialization. This is done by calling

‎Lib/test/test_free_threading/test_type.py‎

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ class A:
2323
def world_stoppers():
2424
"""Finite workloads that contend for the stop-the-world mutex.
2525
26-
Waiting for the mutex releases the type lock, allowing queued type
27-
updates to go stale. Use a short pause rather than full collections
26+
Waiting for the mutex releases the type lock, so that other threads
27+
can change types in the meantime. Use a short pause rather than full collections
2828
to prolong the contention without depending on the size of the heap.
2929
"""
3030
def stopper():
@@ -344,9 +344,8 @@ def wrapper():
344344
reader.join()
345345

346346
def test_setattr_many_subclasses(self):
347-
# gh-155978: Updating a special method queues a slot update for every
348-
# affected subclass. Keep enough subclasses alive to require
349-
# heap-allocated queue chunks in addition to the stack chunk.
347+
# gh-155978: Updating a special method updates the slot of every
348+
# affected subclass.
350349
class Base:
351350
pass
352351

@@ -363,8 +362,7 @@ def custom_repr(self):
363362
self.assertTrue(all(repr(cls()) != "custom repr"
364363
for cls in subclasses))
365364

366-
# The slot update queue must release its references to the types,
367-
# including those stored in heap-allocated chunks.
365+
# The slot update must not keep references to the types.
368366
del subclasses
369367
gc.collect()
370368
self.assertEqual(Base.__subclasses__(), [])
@@ -492,11 +490,45 @@ class C(A, metaclass=Meta):
492490
C.__bases__ = (B,)
493491
self.assertEqual(C.__mro__, (C, B, object))
494492

493+
def test_slot_update_skips_non_str_keys(self):
494+
# Slots are computed (with the world stopped, for an update) without
495+
# comparing the slot name with non-str keys of the type dicts, which
496+
# could run user code. Here, that code would also update a slot
497+
# every time.
498+
calls = 0
499+
500+
class Other:
501+
pass
502+
503+
class Key:
504+
def __hash__(self):
505+
return hash("__hash__")
506+
507+
def __eq__(self, other):
508+
nonlocal calls
509+
calls += 1
510+
Other.__repr__ = lambda self: "x"
511+
return False
512+
513+
class Base:
514+
pass
515+
516+
with self.assertWarns(RuntimeWarning):
517+
Sub = type("Sub", (Base,), {Key(): None})
518+
self.assertEqual(calls, 0)
519+
520+
Base.__hash__ = None
521+
self.assertEqual(calls, 0)
522+
with self.assertRaises(TypeError):
523+
hash(Sub())
524+
del Base.__hash__
525+
hash(Sub())
526+
495527
def test_concurrent_bases_assignment(self):
496528
# Several threads reassign __bases__ of the same class, and of its
497-
# base, while others stop the world. The validated bases and the
498-
# computed MRO can go stale while the type lock is released and
499-
# must be recomputed. At the end, the MRO must agree with __bases__.
529+
# base, while others stop the world. The type lock is released while
530+
# waiting for the stop-the-world. At the end, the MRO must agree
531+
# with __bases__.
500532
N = 50
501533

502534
class A:

‎Objects/dictobject.c‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2631,6 +2631,81 @@ PyDict_GetItemRef(PyObject *op, PyObject *key, PyObject **result)
26312631
return _PyDict_GetItemRef_KnownHash((PyDictObject *)op, key, hash, result);
26322632
}
26332633

2634+
static inline int
2635+
compare_generic_exact_unicode(PyDictObject *mp, PyDictKeysObject *dk,
2636+
void *ep0, Py_ssize_t ix, PyObject *key,
2637+
Py_hash_t hash)
2638+
{
2639+
PyDictKeyEntry *ep = &((PyDictKeyEntry *)ep0)[ix];
2640+
assert(ep->me_key != NULL);
2641+
if (ep->me_key == key) {
2642+
return 1;
2643+
}
2644+
return (ep->me_hash == hash && PyUnicode_CheckExact(ep->me_key) &&
2645+
unicode_eq(ep->me_key, key));
2646+
}
2647+
2648+
// Look up an exact str key, without comparing it to keys that are not exact
2649+
// str: those are skipped, so no user code can run. Returns a borrowed
2650+
// reference, or NULL (without an exception) if the key is not found. The
2651+
// caller must hold the dict lock or have the world stopped.
2652+
PyObject *
2653+
_PyDict_GetItem_ExactUnicode(PyDictObject *mp, PyObject *key)
2654+
{
2655+
assert(PyUnicode_CheckExact(key));
2656+
Py_hash_t hash = hash_unicode_key(key);
2657+
PyDictKeysObject *dk = mp->ma_keys;
2658+
Py_ssize_t ix;
2659+
if (dk->dk_kind == DICT_KEYS_GENERAL) {
2660+
ix = do_lookup(mp, dk, key, hash, compare_generic_exact_unicode);
2661+
}
2662+
else {
2663+
ix = unicodekeys_lookup_unicode(dk, key, hash);
2664+
}
2665+
if (ix < 0) {
2666+
return NULL;
2667+
}
2668+
if (dk->dk_kind == DICT_KEYS_SPLIT) {
2669+
return mp->ma_values->values[ix];
2670+
}
2671+
if (dk->dk_kind == DICT_KEYS_GENERAL) {
2672+
return DK_ENTRIES(dk)[ix].me_value;
2673+
}
2674+
return DK_UNICODE_ENTRIES(dk)[ix].me_value;
2675+
}
2676+
2677+
// Like _PyDict_GetItem_ExactUnicode() but safe to call without holding the
2678+
// dict lock. Stores a new reference (or NULL) in *value_addr. Returns 1 if
2679+
// the key was found and 0 if not.
2680+
int
2681+
_PyDict_GetItemStackRef_ExactUnicode(PyDictObject *mp, PyObject *key,
2682+
_PyStackRef *value_addr)
2683+
{
2684+
assert(PyUnicode_CheckExact(key));
2685+
PyObject *value;
2686+
#ifdef Py_GIL_DISABLED
2687+
ensure_shared_on_read(mp);
2688+
PyDictKeysObject *dk = _Py_atomic_load_ptr_acquire(&mp->ma_keys);
2689+
if (dk->dk_kind == DICT_KEYS_UNICODE) {
2690+
// All keys are exact str, so the lock-free lookup cannot run user
2691+
// code.
2692+
Py_hash_t hash = hash_unicode_key(key);
2693+
Py_ssize_t ix = lookup_threadsafe_unicode(dk, key, hash, value_addr);
2694+
if (ix != DKIX_KEY_CHANGED) {
2695+
return !PyStackRef_IsNull(*value_addr);
2696+
}
2697+
}
2698+
Py_BEGIN_CRITICAL_SECTION(mp);
2699+
value = _PyDict_GetItem_ExactUnicode(mp, key);
2700+
*value_addr = value ? PyStackRef_FromPyObjectNew(value) : PyStackRef_NULL;
2701+
Py_END_CRITICAL_SECTION();
2702+
#else
2703+
value = _PyDict_GetItem_ExactUnicode(mp, key);
2704+
*value_addr = value ? PyStackRef_FromPyObjectNew(value) : PyStackRef_NULL;
2705+
#endif
2706+
return value != NULL;
2707+
}
2708+
26342709
int
26352710
_PyDict_GetItemRef_Unicode_LockHeld(PyDictObject *op, PyObject *key, PyObject **result)
26362711
{

0 commit comments

Comments
 (0)