Mutex<T> in Rust: RwLock, Data Races & Deadlocks

Max WellsMax WellsFounder of Rustify

TL;DR: Mutex<T> (mutual exclusion lock) in Rust wraps a value T and ensures only one thread can access it at a time. You access the inner value by calling .lock(), which blocks until the lock is available and returns a MutexGuard<T>. The guard releases the lock automatically when it goes out of scope, no manual unlock needed. Combined with Arc<T> for shared ownership, Arc<Mutex<T>> is the standard pattern for shared mutable state across threads in Rust.


What Is Mutex<T> in Rust?

Mutex<T> wraps a value and enforces mutual exclusion, only one thread can hold the lock and access the data at a time, preventing data races.

use std::sync::Mutex;
 
fn main() {
    let m = Mutex::new(5); // wraps the value 5
 
    {
        let mut val = m.lock().unwrap(); // acquire lock
        *val += 1;                       // modify the value
        // MutexGuard dropped here; lock released automatically
    }
 
    println!("Value: {:?}", m); // Mutex { data: 6 }
}

Unlike languages where locks are separate from data, Rust's Mutex<T> bundles the lock and the data, you cannot access the inner T without going through .lock(). This makes it impossible to forget to lock before accessing shared data.


How Do You Share a Mutex Across Threads?

Mutex<T> alone cannot be shared across threads, it needs Arc<T> to give each thread its own reference to the same Mutex.

use std::sync::{Arc, Mutex};
use std::thread;
 
fn main() {
    let shared = Arc::new(Mutex::new(Vec::<i32>::new()));
    let mut handles = vec![];
 
    for i in 0..5 {
        let shared = Arc::clone(&shared);
        let handle = thread::spawn(move || {
            let mut vec = shared.lock().unwrap();
            vec.push(i);
        });
        handles.push(handle);
    }
 
    for handle in handles {
        handle.join().unwrap();
    }
 
    let result = shared.lock().unwrap();
    println!("{:?}", *result); // [0, 1, 2, 3, 4] (order may vary)
}

Each thread calls Arc::clone to get a pointer to the same Mutex. Only one thread at a time can hold the MutexGuard, others block at .lock().


What Is RwLock and When Should You Use It?

RwLock<T> allows many concurrent readers or one exclusive writer, better than Mutex for read-heavy workloads.

use std::sync::{Arc, RwLock};
use std::thread;
 
fn main() {
    let data = Arc::new(RwLock::new(vec![1, 2, 3]));
 
    // Multiple threads can read simultaneously
    let mut handles = vec![];
    for _ in 0..3 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            let read_guard = data.read().unwrap(); // shared read lock
            println!("{:?}", *read_guard);
        }));
    }
 
    // One thread writes exclusively
    let data_write = Arc::clone(&data);
    handles.push(thread::spawn(move || {
        let mut write_guard = data_write.write().unwrap(); // exclusive write lock
        write_guard.push(4);
    }));
 
    for h in handles { h.join().unwrap(); }
}
Mutex<T>RwLock<T>
Concurrent readers❌ (one at a time)
Concurrent writers
ComplexityLowerHigher (writer starvation risk)
Use whenWrites frequentReads dominant

How Does Rust Prevent Data Races?

Rust's type system enforces that Mutex is the only way to get mutable access to shared data, the compiler prevents all unsynchronized concurrent mutation.

use std::sync::Mutex;
use std::thread;
 
fn main() {
    let data = Mutex::new(0);
 
    // This CANNOT compile; `data` is not Send if captured by reference
    // thread::spawn(|| { *data.lock().unwrap() += 1; });
 
    // Must use Arc to share across threads
    let data = std::sync::Arc::new(Mutex::new(0));
    let data2 = std::sync::Arc::clone(&data);
    thread::spawn(move || { *data2.lock().unwrap() += 1; });
}

In C++, nothing stops you from writing to shared data without a lock, the bug silently corrupts memory. In Rust, attempting to share unsynchronized mutable data is a compile error.


What Is a Mutex Deadlock and How Do You Avoid It?

A deadlock occurs when two threads each hold one lock and wait for the other's lock, neither can proceed. Rust prevents data races but cannot prevent deadlocks.

use std::sync::{Arc, Mutex};
 
fn main() {
    let lock_a = Arc::new(Mutex::new(0));
    let lock_b = Arc::new(Mutex::new(0));
 
    // Thread 1: acquires A, then tries B
    // Thread 2: acquires B, then tries A
    // → Deadlock if they run simultaneously
 
    // Prevention: always acquire locks in the same order
    // Or: use a single Mutex wrapping both values
    let combined = Arc::new(Mutex::new((0, 0)));
}

Also avoid holding a MutexGuard across an .await in async code, use tokio::sync::Mutex instead, which is designed to be held across await points.


Frequently Asked Questions

.lock() returns Result<MutexGuard, PoisonError>. A mutex is "poisoned" if a thread panicked while holding the lock, the PoisonError signals that the data may be in an inconsistent state. .unwrap() panics on a poisoned mutex. In production, handle it: m.lock().unwrap_or_else(|e| e.into_inner()).

Avoid std::sync::Mutex across .await points, it blocks the thread, which defeats async. Use tokio::sync::Mutex for async code. For non-async critical sections in async code, std::sync::Mutex is fine as long as you release the guard before the next .await.

If a thread panics while holding a Mutex, the mutex is marked "poisoned." Subsequent .lock() calls return Err(PoisonError). This signals that the protected data may be in a corrupt state. Recover with .into_inner() if you know the data is still valid.

The parking_lot crate provides Mutex and RwLock that are faster and smaller than std::sync equivalents. They also do not poison, .lock() always succeeds. Many Rust projects use parking_lot as a drop-in replacement.


Sources


  • Arc: Arc<Mutex<T>> is the standard shared mutable state pattern
  • Ownership: Mutex moves ownership; .lock() temporarily borrows the inner value
  • Async/Await: Use tokio::sync::Mutex in async contexts
  • Trait: Mutex<T> implements Send + Sync when T: Send
  • RwLock: Prefer RwLock when reads dominate and writes are rare
  • Interior Mutability: Mutex<T> is the thread-safe form of Rust's interior mutability pattern

Keep Reading

Ready to Land a $120k+ Rust Job in the US or Europe?