Yet Another Performance Footgun in Rust - Multithreading Lock Edition
It turns out that RAII can lead to performance "footguns", not just regarding memory, but also with multithreading locks.
The "holding a mutex longer than necessary" footgun.
It’s easy to fall into this trap :
{
let data = data.lock()
data.update(10)
// ...lots of other code...
}
Oops, the mutex remains locked for the entire duration of the scope, even though it only needed to be held until the final write to the `data` variable. This degrades performance because other threads are forced to wait longer than they should. The performance hit, latency and throughput, scales linearly with the amount of code executed after the last write, the longer that code takes to reach the end of the scope, the more severe the latency increase and throughput drop become.
This footgun appears frequently in real world code, as many developers write "vanilla" Rust without manually managing the lifetime of these locks. One might suggest calling `drop` explicitly or wrapping the logic in an inner scope, but neither is a robust or reliable solution. Why? Because both require manual awareness, in other words, they rely on the developer's skill to do so. By definition, a "footgun" is something that catches users off guard, often without them even realizing it.
So, what is the solution? As it turns out, "ASAP drop" (dropping as soon as possible) can resolve not only memory related footguns but also performance issues stemming from multithreading locks.
Visualization :
let data = data.lock().unwrap() // `data` becomes a `MutexGuard<T>`
data.update(10)
data.update(20)
// The compiler analyzes when the `MutexGuard<data>` is last used.
// If the code above represents the final access, the compiler automatically inserts an unlock operation immediately afterward.
// This allows other threads to take their turn without any unnecessary, prolonged waiting.
data.unlock() // Inserted by the compiler
As a further implementation guide, since read access is safe to perform from multiple threads, the unlock operation is inserted immediately after the final write, detected via `&mut` references, interior mutability, and raw mutable pointers (`*mut`). The final read access, using `&` and `*const`, does not extend the lifetime of the `MutexGuard`. This ensures that other threads get their turn as soon as possible, eliminating wasted time, resulting in low latency and high throughput right out of the box.