In this lesson, you will learn why recursive locks are often discouraged in Rust and how to restructure your code to achieve the same goals safely.
The Problem: Re-entrancy
A recursive lock (or re-entrant lock) allows a thread that already holds a lock to acquire it again without blocking. While this sounds convenient for complex call graphs where a function might be called by another function that already holds the lock, it introduces significant risks:
- Hidden State Invariants: A recursive lock can allow a function to modify data while it is in an inconsistent state, simply because the programmer assumed the lock was "already held."
- Increased Complexity: If you must release the lock exactly as many times as you acquired it, the logic for managing state becomes fragile and error-prone.
- Deadlock Risks: Recursive locks often mask architectural flaws, leading to subtle deadlocks where dependencies between functions become circular and impossible to trace.
The Rust Pattern: Separation of Concerns
Rust’s ownership model encourages you to avoid recursive locking by separating your internal logic from your public, thread-safe API. Instead of having a public method try to re-acquire a lock, split the logic into two parts:
- The Public Method: Handles the locking and calls the internal function.
- The Private/Internal Method: Operates on the raw data (the locked inner type), expecting the caller to have already handled the synchronization.
struct Data {
counter: i32,
}
impl Data {
// Public API: Synchronizes access
fn increment(&mut self) {
self.internal_increment();
}
// Internal API: Expects the caller to hold the lock/mut reference
fn internal_increment(&mut self) {
self.counter += 1;
}
// Complex operation reusing internal logic
fn batch_increment(&mut self) {
self.internal_increment();
self.internal_increment();
}
}
By passing a mutable reference &mut self to internal methods, the compiler enforces that these methods can only be called from contexts where you already have the required access. This makes the dependency on the lock explicit rather than hidden inside the lock implementation itself.
Key Takeaway: Rather than nesting lock acquisitions, favor factoring your logic into private helper methods that operate on borrowed data, making state transitions explicit and easy to audit.