Skip to content

Commit 4653f4d

Browse files
committed
Strengthen type-lock regression tests and reduce their cost
Use explicit stop-the-world pauses and finite worker workloads. Replace slot-update stress with a deterministic reentrancy test, and consolidate queue reference-release coverage.
1 parent 6c71103 commit 4653f4d

3 files changed

Lines changed: 138 additions & 124 deletions

File tree

‎Lib/test/test_free_threading/test_threading.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,30 @@ def mutate_thread():
2626

2727

2828
class TestThreadState(unittest.TestCase):
29+
def test_stop_the_world(self):
30+
import _testinternalcapi
31+
32+
_testinternalcapi.stop_the_world()
33+
_testinternalcapi.stop_the_world(0)
34+
35+
def pause():
36+
for _ in range(10):
37+
_testinternalcapi.stop_the_world(0.0001)
38+
39+
threading_helper.run_concurrently(pause, nthreads=2)
40+
for duration in (-1, -0.1, float("nan")):
41+
with self.subTest(duration=duration):
42+
with self.assertRaises(ValueError):
43+
_testinternalcapi.stop_the_world(duration)
44+
for duration in (float("inf"), 10**100):
45+
with self.subTest(duration=duration):
46+
with self.assertRaises(OverflowError):
47+
_testinternalcapi.stop_the_world(duration)
48+
for duration in (None, "0"):
49+
with self.subTest(duration=duration):
50+
with self.assertRaises(TypeError):
51+
_testinternalcapi.stop_the_world(duration)
52+
2953
@support.requires_subprocess()
3054
def test_tight_stw_loop_does_not_starve_attach(self):
3155
script = textwrap.dedent(f"""

‎Lib/test/test_free_threading/test_type.py‎

Lines changed: 76 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import gc
22
import threading
33
import unittest
4+
import weakref
45

56
from concurrent.futures import ThreadPoolExecutor
67
from threading import Barrier, Thread
@@ -20,25 +21,22 @@
2021
class A:
2122
attr = 1
2223

23-
def world_stopper_threads(done):
24-
"""Threads that stop the world until 'done' is set.
24+
def world_stoppers():
25+
"""Finite workloads that contend for the stop-the-world mutex.
2526
26-
The stopper keeps the stop-the-world mutex contended, so that threads
27-
updating types block on it and release the type lock. The collector
28-
makes the stop-the-world pauses long, so that other threads have time
29-
to update types while the lock is released.
27+
Waiting for the mutex releases the type lock, allowing queued type
28+
updates to go stale. Use a short pause rather than full collections
29+
to prolong the contention without depending on the size of the heap.
3030
"""
3131
def stopper():
32-
def g():
33-
pass
34-
while not done.is_set():
35-
g.__qualname__ = "g" # stops the world
32+
for _ in range(100):
33+
_testinternalcapi.stop_the_world()
3634

37-
def collector():
38-
while not done.is_set():
39-
gc.collect()
35+
def pauser():
36+
for _ in range(100):
37+
_testinternalcapi.stop_the_world(0.0001)
4038

41-
return [Thread(target=stopper), Thread(target=collector)]
39+
return [stopper, pauser]
4240

4341

4442
@threading_helper.requires_working_threading()
@@ -366,6 +364,12 @@ def custom_repr(self):
366364
self.assertTrue(all(repr(cls()) != "custom repr"
367365
for cls in subclasses))
368366

367+
# The slot update queue must release its references to the types,
368+
# including those stored in heap-allocated chunks.
369+
del subclasses
370+
gc.collect()
371+
self.assertEqual(Base.__subclasses__(), [])
372+
369373
def test_concurrent_setattr_deadlock(self):
370374
# gh-155400: two threads assigning to a special method of the same
371375
# class could deadlock. One thread held the type lock and waited for
@@ -430,50 +434,34 @@ def check_stop_the_world_under_type_lock(self, setter):
430434
# assignment). The first thread released the type lock while it was
431435
# blocked on the stop-the-world mutex, was handed the mutex, and then
432436
# waited for the type lock while owning the mutex.
433-
done = threading.Event()
434-
435-
def run_setter():
436-
barrier.wait()
437-
setter()
438-
439437
def basesetter():
440438
class A:
441439
pass
442440
class B:
443441
pass
444442
class C(A):
445443
pass
446-
barrier.wait()
447-
while not done.is_set():
444+
for _ in range(100):
448445
C.__bases__ = (B,)
449446
C.__bases__ = (A,)
450447

451448
def stopper():
452-
def g():
453-
pass
454-
barrier.wait()
455-
while not done.is_set():
456-
g.__qualname__ = "g" # stops the world
457-
458-
# The stopper thread keeps the stop-the-world mutex contended, which
459-
# makes the setters block on it.
460-
setters = [Thread(target=run_setter) for _ in range(2)]
461-
others = [Thread(target=basesetter), Thread(target=stopper)]
462-
barrier = threading.Barrier(len(setters) + len(others))
463-
with threading_helper.start_threads(setters + others):
464-
for t in setters:
465-
t.join()
466-
done.set()
449+
for _ in range(100):
450+
_testinternalcapi.stop_the_world()
451+
452+
# The stopper keeps the stop-the-world mutex contended, which makes
453+
# the setters block on it. All workers have finite workloads so
454+
# that repeated invalidation cannot indefinitely delay a setter.
455+
threading_helper.run_concurrently(
456+
[setter, setter, basesetter, stopper])
467457

468458
def test_stop_the_world_in_mro_deadlock(self):
469459
# type_ready() calls a metaclass mro() with the type lock held.
470460
N = 50
471-
def f():
472-
pass
473461

474462
class Meta(type):
475463
def mro(cls):
476-
f.__qualname__ = "f" # stops the world
464+
_testinternalcapi.stop_the_world()
477465
return type.mro(cls)
478466

479467
def setter():
@@ -486,13 +474,11 @@ def test_stop_the_world_in_setattr_key_eq_deadlock(self):
486474
# type_setattro() compares the attribute name with a str subclass key
487475
# of the type's __dict__ (calling its __eq__) with the type lock held.
488476
N = 50
489-
def f():
490-
pass
491477

492478
class S(str):
493479
__hash__ = str.__hash__
494480
def __eq__(self, other):
495-
f.__qualname__ = "f" # stops the world
481+
_testinternalcapi.stop_the_world()
496482
return str.__eq__(self, other)
497483

498484
C = type("C", (), {S("x"): 0})
@@ -503,89 +489,63 @@ def setter():
503489

504490
self.check_stop_the_world_under_type_lock(setter)
505491

506-
def test_concurrent_slot_update_consistency(self):
507-
# Several threads assign and delete the same special method of a
508-
# class with subclasses while other threads stop the world. The
509-
# type lock is released while a thread waits for the stop-the-world,
510-
# so the slot updates it prepared can go stale and must be redone.
511-
# At the end, the type slots must agree with the class dicts.
512-
N = 300
513-
done = threading.Event()
492+
def test_reentrant_slot_update_consistency(self):
493+
# Updating Base queues its slot update before visiting Sub. A key
494+
# comparison in Sub's dict re-enters the setter and makes that queue
495+
# stale. It must be rebuilt rather than overwriting the inner update.
496+
armed = False
514497

515498
class Base:
516499
pass
517500

518-
subclasses = [type(f"Sub{i}", (Base,), {}) for i in range(5)]
519-
funcs = [lambda self, i=i: f"repr{i}" for i in range(4)]
520-
521-
def setter(i):
522-
barrier.wait()
523-
for _ in range(N):
524-
Base.__repr__ = funcs[i]
525-
try:
526-
del Base.__repr__
527-
except AttributeError:
528-
pass
529-
530-
setters = [Thread(target=setter, args=(i,)) for i in range(len(funcs))]
531-
others = world_stopper_threads(done)
532-
barrier = threading.Barrier(len(setters))
533-
with threading_helper.start_threads(setters + others):
534-
for t in setters:
535-
t.join()
536-
done.set()
537-
538-
for cls in [Base] + subclasses:
539-
if "__repr__" in Base.__dict__:
540-
expected = Base.__dict__["__repr__"](cls())
541-
self.assertEqual(repr(cls()), expected)
542-
else:
543-
# object.__repr__ is used
544-
self.assertStartsWith(repr(cls()), "<")
545-
546-
def test_slot_update_subclass_freed(self):
547-
# Subclasses are created and freed while another thread updates a
548-
# slot that is inherited by them. The slot update queue must keep
549-
# the subclasses alive until the update is applied, since the type
550-
# lock is released while waiting for the stop-the-world.
551-
N = 300
552-
done = threading.Event()
501+
class Key(str):
502+
def __hash__(self):
503+
return hash("__hash__")
553504

554-
class Base:
555-
pass
556-
557-
def setter():
558-
func = lambda self: "x"
559-
barrier.wait()
560-
for _ in range(N):
561-
Base.__repr__ = func
562-
try:
563-
del Base.__repr__
564-
except AttributeError:
565-
pass
505+
def __eq__(self, other):
506+
nonlocal armed
507+
if armed and other == "__hash__":
508+
armed = False
509+
Base.__hash__ = object.__hash__
510+
return str.__eq__(self, other)
566511

567-
def subclasser():
568-
barrier.wait()
569-
while not done.is_set():
570-
for _ in range(20):
571-
type("Sub", (Base,), {})
572-
573-
setters = [Thread(target=setter) for _ in range(2)]
574-
others = ([Thread(target=subclasser), Thread(target=subclasser)]
575-
+ world_stopper_threads(done))
576-
barrier = threading.Barrier(len(setters))
577-
with threading_helper.start_threads(setters + others):
578-
for t in setters:
579-
t.join()
580-
done.set()
512+
# Collide with the special-method name without defining it on Sub,
513+
# so that slot updates still propagate from Base to Sub.
514+
Sub = type("Sub", (Base,), {Key("trigger"): None})
515+
objects = [Base(), Sub()]
516+
517+
# Check actual slot dispatch, not just the class dictionaries. None
518+
# and object.__hash__ use distinct C slots, unlike Python functions
519+
# which share a generic wrapper that looks up the current method.
520+
Base.__hash__ = None
521+
for obj in objects:
522+
with self.assertRaises(TypeError):
523+
hash(obj)
524+
Base.__hash__ = object.__hash__
525+
for obj in objects:
526+
self.assertEqual(hash(obj), object.__hash__(obj))
527+
528+
armed = True
529+
Base.__hash__ = None
530+
self.assertFalse(armed)
531+
self.assertIs(Base.__dict__["__hash__"], object.__hash__)
532+
for obj in objects:
533+
self.assertEqual(hash(obj), object.__hash__(obj))
534+
535+
# Both applied and discarded queues must release their references.
536+
base_ref = weakref.ref(Base)
537+
sub_ref = weakref.ref(Sub)
538+
del obj, objects, Sub, Base
539+
gc.collect()
540+
self.assertIsNone(base_ref())
541+
self.assertIsNone(sub_ref())
581542

582543
def test_concurrent_bases_assignment(self):
583544
# Several threads reassign __bases__ of the same class, and of its
584545
# base, while others stop the world. The validated bases and the
585546
# computed MRO can go stale while the type lock is released and
586547
# must be recomputed. At the end, the MRO must agree with __bases__.
587-
N = 100
588-
done = threading.Event()
548+
N = 50
589549

590550
class A:
591551
def meth(self):
@@ -603,25 +563,17 @@ class D(C):
603563
pass
604564

605565
def setter():
606-
barrier.wait()
607566
for _ in range(N):
608567
C.__bases__ = (B,)
609568
C.__bases__ = (A,)
610569

611570
def subsetter():
612-
barrier.wait()
613571
for _ in range(N):
614572
D.__bases__ = (C, B)
615573
D.__bases__ = (C,)
616574

617-
setters = [Thread(target=setter), Thread(target=setter),
618-
Thread(target=subsetter)]
619-
others = world_stopper_threads(done)
620-
barrier = threading.Barrier(len(setters))
621-
with threading_helper.start_threads(setters + others):
622-
for t in setters:
623-
t.join()
624-
done.set()
575+
threading_helper.run_concurrently(
576+
[setter, setter, subsetter] + world_stoppers())
625577

626578
self.assertEqual(C.__bases__, (A,))
627579
self.assertEqual(C.__mro__, (C, A, object))

‎Modules/_testinternalcapi.c‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
#include "pycore_pylifecycle.h" // _PyInterpreterConfig_InitFromDict()
3939
#include "pycore_pystate.h" // _PyThreadState_GET()
4040
#include "pycore_runtime_structs.h" // _PY_NSMALLPOSINTS
41+
#include "pycore_time.h" // _PyTime_FromSecondsObject()
4142
#include "pycore_unicodeobject.h" // _PyUnicode_TransformDecimalAndSpaceToASCII()
4243

4344
#include "clinic/_testinternalcapi.c.h"
@@ -209,6 +210,42 @@ get_stack_margin(PyObject *self, PyObject *Py_UNUSED(args))
209210
return PyLong_FromSize_t(_PyOS_STACK_MARGIN_BYTES);
210211
}
211212

213+
static PyObject *
214+
stop_the_world(PyObject *self, PyObject *args)
215+
{
216+
PyObject *duration_obj = NULL;
217+
PyTime_t duration = 0;
218+
if (!PyArg_ParseTuple(args, "|O:stop_the_world", &duration_obj)) {
219+
return NULL;
220+
}
221+
if (duration_obj != NULL &&
222+
_PyTime_FromSecondsObject(&duration, duration_obj,
223+
_PyTime_ROUND_TIMEOUT) < 0) {
224+
return NULL;
225+
}
226+
if (duration < 0) {
227+
PyErr_SetString(PyExc_ValueError, "duration must be non-negative");
228+
return NULL;
229+
}
230+
#ifdef Py_GIL_DISABLED
231+
PyInterpreterState *interp = _PyInterpreterState_GET();
232+
_PyEval_StopTheWorld(interp);
233+
if (duration != 0) {
234+
PyEvent event = {0};
235+
PyEvent_WaitTimed(&event, duration, /*detach=*/0);
236+
}
237+
_PyEval_StartTheWorld(interp);
238+
#endif
239+
Py_RETURN_NONE;
240+
}
241+
242+
PyDoc_STRVAR(stop_the_world_doc,
243+
"stop_the_world($module, duration=0, /)\n"
244+
"--\n"
245+
"\n"
246+
"Stop the world, wait for duration seconds, then restart it.\n"
247+
"Does nothing in GIL-enabled builds.");
248+
212249
static PyObject *
213250
test_stop_the_world(PyObject *self, PyObject *Py_UNUSED(args))
214251
{
@@ -3345,6 +3382,7 @@ static PyMethodDef module_functions[] = {
33453382
{"get_c_recursion_remaining", get_c_recursion_remaining, METH_NOARGS},
33463383
{"get_stack_pointer", get_stack_pointer, METH_NOARGS},
33473384
{"get_stack_margin", get_stack_margin, METH_NOARGS},
3385+
{"stop_the_world", stop_the_world, METH_VARARGS, stop_the_world_doc},
33483386
{"test_stop_the_world", test_stop_the_world, METH_NOARGS},
33493387
{"classify_stack_addresses", classify_stack_addresses, METH_VARARGS},
33503388
{"get_jit_code_ranges", get_jit_code_ranges, METH_NOARGS},

0 commit comments

Comments
 (0)