In this lesson, you will learn how to design locking hierarchies and protocols to prevent threads from waiting indefinitely on each other.
A deadlock occurs when two or more threads are stuck in a "circular wait" condition: Thread A holds Lock 1 and waits for Lock 2, while Thread B holds Lock 2 and waits for Lock 1. Neither can proceed, and the application hangs. To prevent this, we use a strategy called Lock Ordering.
Establishing a Lock Hierarchy
The simplest way to avoid circular waits is to define a strict global order for acquiring locks. If your application needs to acquire both Mutex_A and Mutex_B, every thread must be programmed to lock them in the exact same sequence—for example, always A then B. If a thread needs B but doesn't have A, it must be designed to either release B or ensure it follows the hierarchy.
Consider this scenario where two threads attempt to update two different Mutex values:
use std::sync::Mutex;
let lock_a = Mutex::new(0);
let lock_b = Mutex::new(0);
// Thread 1 and Thread 2 both agree: always lock A, then B.
let _a = lock_a.lock().unwrap();
let _b = lock_b.lock().unwrap();
By enforcing that all threads acquire lock_a before lock_b, it becomes impossible for a circular dependency to form. Thread 2 will simply block at lock_a before it ever reaches the point where it could hold lock_b and block Thread 1.
Fine-Grained Locking Protocols
When a global hierarchy is too restrictive, we use Try-Lock protocols. Instead of waiting indefinitely for a lock, a thread attempts to acquire it and, if it fails, it rolls back its state:
if let Ok(guard_a) = lock_a.try_lock() {
if let Ok(guard_b) = lock_b.try_lock() {
// Perform work
} else {
// Release guard_a and retry later
}
}
By using try_lock(), you convert a "wait indefinitely" scenario into a "retry later" scenario, which effectively breaks the hold-and-wait condition required for a deadlock to persist. Always favor clear, consistent locking orders in your architecture to keep your concurrent systems predictable and hang-free.
Key Takeaway: Prevent deadlocks by establishing a strict global hierarchy for lock acquisition or by using non-blocking retry protocols when a resource is unavailable.