rust-mutex-estado-compartido

Shared State in Rust with Mutex and Arc<Mutex<T>>

  • 4 min

A Mutex is a mechanism that serializes access to data across multiple threads.

Mutex is short for Mutual Exclusion. Imagine there’s only one microphone in a conference room. Only one person can speak at a time. If someone wants to speak, they must wait until the current speaker releases the microphone.

In Rust, a Mutex<T> protects a piece of data T. If a thread wants to access the data, it must request permission (“lock” the Mutex).

The Mutex<T> API

Mutex works similarly to RefCell: it provides Interior Mutability. The data inside is immutable, but the Mutex allows us to safely obtain a mutable reference.

use std::sync::Mutex;

fn main() {
    // Create a Mutex protecting an integer
    let m = Mutex::new(5);

    {
        // 1. Request the lock.
        // This BLOCKS the current thread if someone else is using it.
        // Returns a Result (unwrap is needed due to "lock poisoning", see note).
        let mut num = m.lock().unwrap();

        // 2. Modify the data.
        // 'num' is a Smart Pointer (MutexGuard) that acts like &mut i32.
        *num = 6;

    } // 3. Here 'num' goes out of scope and the lock is automatically released.

    println!("m = {}", *m.lock().unwrap()); // m = 6
}
Copied!

Automatic Unlock In other languages (C++, Java, Go), you must remember to call .unlock(). If you forget (or if an exception occurs before), you create an eternal Deadlock. In Rust, unlocking is tied to the Drop Trait of MutexGuard. As soon as the num variable dies, the lock is released. Safety by default.

Sharing a Mutex Between Threads

Now let’s try using this with actual concurrency. We want 10 threads to increment a counter.

If we try to pass the Mutex directly, we can’t, because the Ownership system won’t let us give the same Mutex to 10 threads (the first one would take it with move).

We need Multiple Owners. Remember Rc<T>? We saw it was useful for this, but it wasn’t thread-safe. For concurrency, we must use its atomic sibling: Arc<T>.

The Arc<Mutex<T>> Pattern

This is the standard combination in Rust for shared mutable state:

  • Arc: Allows multiple threads to own the data.
  • Mutex: Ensures only one modifies it at a time.
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // 1. Create the protected counter (Arc wraps the Mutex)
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        // 2. Clone the Arc for this thread (cheap and atomic)
        let counter_clone = Arc::clone(&counter);

        let handle = thread::spawn(move || {
            // 3. Lock the Mutex to access the number
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
        });

        handles.push(handle);
    }

    // 4. Wait for all threads
    for handle in handles {
        handle.join().unwrap();
    }

    // 5. Read the final result
    println!("Result: {}", *counter.lock().unwrap()); // 10
}
Copied!

It works! 10 threads have incremented the same number without race conditions. Rust forced us to use lock(), preventing us from modifying memory haphazardly.

The Send and Sync Traits

In the previous article we saw the Send trait (safe to send to another thread). There’s another trait called Sync.

  • Send: The type T can be sent (moved) to another thread.
  • Sync: The type T can be shared between threads via references (&T).

The beauty of Mutex is that it can provide synchronized access even if T is not Sync by itself. Specifically, Mutex<T> is Sync when T: Send, because the protected value can be safely transferred between the threads that acquire the lock.

Deadlocks

Although Rust protects us from memory corruption (Race Conditions), it does NOT protect us from incorrect logic. It is still possible to cause a Deadlock.

A Deadlock occurs if:

  1. Thread A locks resource 1 and waits for resource 2.
  2. Thread B locks resource 2 and waits for resource 1.
  3. Both wait forever.
let lock1 = m1.lock().unwrap();
// ... slow operation ...
let lock2 = m2.lock().unwrap(); // If another thread did this in reverse -> Deadlock
Copied!

Rust does not detect this at compile time. It’s your responsibility to design the logic to avoid waiting cycles.