11import gc
22import threading
33import unittest
4+ import weakref
45
56from concurrent .futures import ThreadPoolExecutor
67from threading import Barrier , Thread
2021class 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 ))
0 commit comments