Synchronization Primitives

Locking and Unlocking

In Rust, locking a Mutex<T> is an explicit method call (.lock() or .try_lock()), whereas unlocking is performed automatically via RAII when the returned MutexGuard<'_, T> is dropped (either at the end of its lexical block or via an explicit drop(guard) call). There is no mutex.unlock() method on Mutex<T> itself.

Why Mutex<T> Has No .unlock() Method

If Mutex<T> had a .unlock() method while a MutexGuard<'_, T> or a reference &mut T derived from the guard was still in scope, calling mutex.unlock() would release the lock while the caller continued mutating &mut T concurrently with the next thread that locked the mutex!

By tying the lock state strictly to the lifetime 'a of MutexGuard<'a, T>:

  1. Any reference &'b mut T borrowed from the guard cannot outlive the guard ('a: 'b).
  2. Dropping the guard (drop(guard)) requires that no active &mut T borrow derived from guard is still in use!
Lifetime Enforcement of `MutexGuard<'a, T>`:

  let mut guard: MutexGuard<'_, Vec<i32>> = mutex.lock().unwrap();
  let elem_ref: &mut i32 = &mut guard[0];
  drop(guard);        // <-- Compile Error E0505 if `elem_ref` is used after this line!
  *elem_ref = 99;     //     (Prevents mutating inner data after the lock is released!)

Non-Blocking Acquisition with .try_lock()

While .lock() blocks the calling thread indefinitely until the mutex becomes available, .try_lock() attempts to acquire the mutex immediately and returns a TryLockResult<MutexGuard<'_, T>> (Result<MutexGuard<'_, T>, TryLockError<MutexGuard<'_, T>>>):

try_lock() Return Variant Meaning
Ok(guard) Lock was free and is now held by guard
Err(TryLockError::WouldBlock) Another thread (or an earlier guard on the same thread) currently holds the lock
Err(TryLockError::Poisoned(p)) Lock was free, but a previous holder panicked while holding it
use std::sync::{Mutex, TryLockError};
use std::thread;

fn main() {
    let resource = Mutex::new(String::from("idle"));

    // Hold the lock on the main thread:
    let mut main_guard = resource.lock().unwrap();
    *main_guard = String::from("busy-on-main");

    thread::scope(|s| {
        s.spawn(|| {
            // Non-blocking check from a worker thread while `main_guard` is active:
            match resource.try_lock() {
                Ok(guard) => println!("worker acquired lock: {}", *guard),
                Err(TryLockError::WouldBlock) => {
                    println!("worker saw WouldBlock; skipping contended work")
                }
                Err(TryLockError::Poisoned(_)) => println!("lock poisoned"),
            }
        });
    });

    // Explicitly unlock on main thread before acquiring again:
    drop(main_guard);

    println!("after drop(main_guard), try_lock succeeds: {:?}", resource.try_lock().unwrap());
}
worker saw WouldBlock; skipping contended work
after drop(main_guard), try_lock succeeds: "busy-on-main"

Zero-Lock Exclusive Access with Mutex::get_mut and Mutex::into_inner

When a single thread holds an exclusive mutable reference &mut Mutex<T> (for example, after all worker threads in thread::scope have finished or after Arc::try_unwrap succeeds), calling .get_mut() or .into_inner() accesses the inner T with zero atomic instructions because the type system guarantees no other thread has a reference to the mutex:

use std::sync::Mutex;
use std::thread;

fn main() {
    let mut accumulator = Mutex::new(Vec::new());

    thread::scope(|s| {
        for i in 1..=3 {
            let acc_ref = &accumulator;
            s.spawn(move || {
                acc_ref.lock().unwrap().push(i * 10);
            });
        }
    });

    // Now that the scope has ended, `main` has exclusive `&mut accumulator` again.
    // `.get_mut()` sorts in place without locking!
    accumulator.get_mut().unwrap().sort();

    // `.into_inner()` unwraps the `Vec<i32>` out of the `Mutex` by value:
    let final_vec: Vec<i32> = accumulator.into_inner().unwrap();
    println!("final_vec: {:?}", final_vec);
}
final_vec: [10, 20, 30]