|
| 1 | +import gc |
1 | 2 | import threading |
2 | 3 | import unittest |
3 | 4 |
|
|
19 | 20 | class A: |
20 | 21 | attr = 1 |
21 | 22 |
|
| 23 | +def world_stopper_threads(done): |
| 24 | + """Threads that stop the world until 'done' is set. |
| 25 | +
|
| 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. |
| 30 | + """ |
| 31 | + def stopper(): |
| 32 | + def g(): |
| 33 | + pass |
| 34 | + while not done.is_set(): |
| 35 | + g.__qualname__ = "g" # stops the world |
| 36 | + |
| 37 | + def collector(): |
| 38 | + while not done.is_set(): |
| 39 | + gc.collect() |
| 40 | + |
| 41 | + return [Thread(target=stopper), Thread(target=collector)] |
| 42 | + |
| 43 | + |
22 | 44 | @threading_helper.requires_working_threading() |
23 | 45 | class TestType(TestCase): |
24 | 46 | def test_attr_cache(self): |
@@ -401,6 +423,216 @@ class B(A): |
401 | 423 | with threading_helper.start_threads(threads): |
402 | 424 | pass |
403 | 425 |
|
| 426 | + def check_stop_the_world_under_type_lock(self, setter): |
| 427 | + # gh-158195: Python code that runs with the type lock held and stops |
| 428 | + # the world could deadlock with a thread that held the type lock |
| 429 | + # pinned while it waited for the stop-the-world mutex (__bases__ |
| 430 | + # assignment). The first thread released the type lock while it was |
| 431 | + # blocked on the stop-the-world mutex, was handed the mutex, and then |
| 432 | + # waited for the type lock while owning the mutex. |
| 433 | + done = threading.Event() |
| 434 | + |
| 435 | + def run_setter(): |
| 436 | + barrier.wait() |
| 437 | + setter() |
| 438 | + |
| 439 | + def basesetter(): |
| 440 | + class A: |
| 441 | + pass |
| 442 | + class B: |
| 443 | + pass |
| 444 | + class C(A): |
| 445 | + pass |
| 446 | + barrier.wait() |
| 447 | + while not done.is_set(): |
| 448 | + C.__bases__ = (B,) |
| 449 | + C.__bases__ = (A,) |
| 450 | + |
| 451 | + 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() |
| 467 | + |
| 468 | + def test_stop_the_world_in_mro_deadlock(self): |
| 469 | + # type_ready() calls a metaclass mro() with the type lock held. |
| 470 | + N = 50 |
| 471 | + def f(): |
| 472 | + pass |
| 473 | + |
| 474 | + class Meta(type): |
| 475 | + def mro(cls): |
| 476 | + f.__qualname__ = "f" # stops the world |
| 477 | + return type.mro(cls) |
| 478 | + |
| 479 | + def setter(): |
| 480 | + for _ in range(N): |
| 481 | + Meta("K", (), {}) |
| 482 | + |
| 483 | + self.check_stop_the_world_under_type_lock(setter) |
| 484 | + |
| 485 | + def test_stop_the_world_in_setattr_key_eq_deadlock(self): |
| 486 | + # type_setattro() compares the attribute name with a str subclass key |
| 487 | + # of the type's __dict__ (calling its __eq__) with the type lock held. |
| 488 | + N = 50 |
| 489 | + def f(): |
| 490 | + pass |
| 491 | + |
| 492 | + class S(str): |
| 493 | + __hash__ = str.__hash__ |
| 494 | + def __eq__(self, other): |
| 495 | + f.__qualname__ = "f" # stops the world |
| 496 | + return str.__eq__(self, other) |
| 497 | + |
| 498 | + C = type("C", (), {S("x"): 0}) |
| 499 | + |
| 500 | + def setter(): |
| 501 | + for i in range(N): |
| 502 | + C.x = i |
| 503 | + |
| 504 | + self.check_stop_the_world_under_type_lock(setter) |
| 505 | + |
| 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() |
| 514 | + |
| 515 | + class Base: |
| 516 | + pass |
| 517 | + |
| 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() |
| 553 | + |
| 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 |
| 566 | + |
| 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() |
| 581 | + |
| 582 | + def test_concurrent_bases_assignment(self): |
| 583 | + # Several threads reassign __bases__ of the same class, and of its |
| 584 | + # base, while others stop the world. The validated bases and the |
| 585 | + # computed MRO can go stale while the type lock is released and |
| 586 | + # must be recomputed. At the end, the MRO must agree with __bases__. |
| 587 | + N = 100 |
| 588 | + done = threading.Event() |
| 589 | + |
| 590 | + class A: |
| 591 | + def meth(self): |
| 592 | + return "A" |
| 593 | + def __repr__(self): |
| 594 | + return "A" |
| 595 | + class B: |
| 596 | + def meth(self): |
| 597 | + return "B" |
| 598 | + def __repr__(self): |
| 599 | + return "B" |
| 600 | + class C(A): |
| 601 | + pass |
| 602 | + class D(C): |
| 603 | + pass |
| 604 | + |
| 605 | + def setter(): |
| 606 | + barrier.wait() |
| 607 | + for _ in range(N): |
| 608 | + C.__bases__ = (B,) |
| 609 | + C.__bases__ = (A,) |
| 610 | + |
| 611 | + def subsetter(): |
| 612 | + barrier.wait() |
| 613 | + for _ in range(N): |
| 614 | + D.__bases__ = (C, B) |
| 615 | + D.__bases__ = (C,) |
| 616 | + |
| 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() |
| 625 | + |
| 626 | + self.assertEqual(C.__bases__, (A,)) |
| 627 | + self.assertEqual(C.__mro__, (C, A, object)) |
| 628 | + self.assertEqual(D.__bases__, (C,)) |
| 629 | + self.assertEqual(D.__mro__, (D, C, A, object)) |
| 630 | + self.assertEqual(D().meth(), "A") |
| 631 | + self.assertEqual(repr(C()), "A") |
| 632 | + self.assertEqual(repr(D()), "A") |
| 633 | + self.assertIn(C, A.__subclasses__()) |
| 634 | + self.assertNotIn(C, B.__subclasses__()) |
| 635 | + |
404 | 636 |
|
405 | 637 | if __name__ == "__main__": |
406 | 638 | unittest.main() |
0 commit comments