Exercise 2: A Three-Way Circular Deadlock — Possible Solution ==================================================================== THE TEST ------------------------------ lock_mgr = LockManagerV2() barrier = threading.Barrier(3) # guarantees all 3 first-locks complete first results = {} def ta(): lock_mgr.acquire_exclusive('p1', 'A') barrier.wait() results['A'] = lock_mgr.acquire_exclusive('p2', 'A', timeout=1.0) def tb(): lock_mgr.acquire_exclusive('p2', 'B') barrier.wait() results['B'] = lock_mgr.acquire_exclusive('p3', 'B', timeout=1.0) def tc(): lock_mgr.acquire_exclusive('p3', 'C') barrier.wait() results['C'] = lock_mgr.acquire_exclusive('p1', 'C', timeout=1.0) threads = [threading.Thread(target=f) for f in (ta, tb, tc)] for t in threads: t.start() for t in threads: t.join(timeout=3.0) RESULT ------------------------------ A waiting for p2 (held by B): False B waiting for p3 (held by C): False C waiting for p1 (held by A): False All three second-lock attempts fail (time out), exactly like the two-transaction case in Finding 4 -- A holds p1 and wants p2 (held by B), B holds p2 and wants p3 (held by C), C holds p3 and wants p1 (held by A). A -> B -> C -> A: a closed loop of length 3, not 2. WHY THE BARRIER IS NECESSARY HERE (A REAL BUG FOUND WHILE BUILDING THIS TEST) ------------------------------ An earlier version of this test used threading.Event objects instead of a Barrier -- each thread waited only for ONE specific other thread's own event before attempting its second lock, mirroring Finding 4's own two-thread setup. That version genuinely hung: with three threads instead of two, page p3 is touched by BOTH C's first attempt (acquire p3) AND B's second attempt (acquire p3, after waiting only for A, not for C) -- with no guarantee C's own first attempt has actually run yet. If B's thread happened to reach its own second acquire call before C's thread had even started running, B could grab p3 "early", uncontested, since nothing was holding it yet. B would then finish without ever releasing p3 (this test doesn't call release_all()), and when C's thread finally did start and tried its own first acquire on p3, it would block FOREVER -- because C's first acquire call has no timeout at all, unlike the second calls. A three-party threading.Barrier(3) fixes this cleanly: every thread calls barrier.wait() immediately after its own first acquire succeeds, and barrier.wait() doesn't return for ANY thread until all three have reached it. This guarantees every one of the three pages is genuinely held by its intended first owner before any thread attempts a second acquisition on any page -- eliminating the race entirely, rather than trying to out-guess thread scheduling with more Event objects. WHY THIS WORKS AS AN ANSWER ------------------------------ Extending the deadlock cycle from length 2 to length 3 -- and, in the process, discovering and fixing a genuine synchronization bug in the test's OWN setup, not in LockManagerV2 itself -- confirms that _creates_cycle()'s DFS-based approach (walking the whole chain of who's-waiting-on-whom, not just checking a single direct pair) is the right general shape for catching a cycle of any length, and shows concretely why reliably reproducing a multi-party race at all requires real, explicit synchronization rather than assuming threads execute in launch order.