What is a Mutex in Rust?

A mutex, short for Mutual Exclusion, ensures only one thread can access a piece of data at a time. In Rust, a Mutex is used to safely share data between threads without causing race conditions. This means only one thread can “lock” the data at a time, and other threads must wait until the data is unlocked.

In multithreading, two threads try to change the same data at the same time, which creates a problem like two people trying to write on the same paper at once. A Mutex prevents that by ensuring only one thread at a time can use the data.

How Does a Mutex Work?

A Mutex is like a lock on a treasure box that many people (threads) want to open. But to keep the treasure safe, Rust use the three phases, such as:

  1. Locking: Only one person can take the key and open the box at a time. In Rust, when a thread calls .lock(), it gets exclusive access to the data.
  2. Unlocking: After using the treasure, that person returns the key (the lock is released). When the thread finishes using the data, Rust automatically unlocks the mutex.
  3. Thread Safety: Rust ensures that threads access the data safely, preventing race conditions. Because only one thread holds the key, no two threads can change the data at the same time.

Simple example:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
let data = Arc::new(Mutex::new(100)); // Shared data with a lock
let mut handles = vec![];

for _ in 0..3 {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
// Step 1: Lock the mutex to get access
let mut number = data_clone.lock().unwrap();
println!("Thread got the lock. Current value: {}", *number);

// Step 2: Modify the data safely
*number += 10;

// Step 3: Mutex unlocks automatically here when 'number' goes out of scope
});
handles.push(handle);
}

for handle in handles {
handle.join().unwrap();
}

println!("Final Value: {}", *data.lock().unwrap());
}

How To Create and Using a Mutex In Rust?

To use a Mutex, you first wrap your data in Mutex::new(). Then, use the lock() method to access or modify the data.

Example: Basic Mutex

use std::sync::Mutex;

fn main() {
// Step 1: Put data (0) inside a Mutex (like locking it in a safe)
let balance = Mutex::new(0);

{
// Step 2: Lock the mutex to safely access the data
let mut money = balance.lock().unwrap();

println!("Before deposit: {}", *money);

// Step 3: Safely modify the data
*money += 500;

println!("After deposit: {}", *money);

// Step 4: Mutex will unlock automatically here (end of block)
}

// Final check: Lock again if you want to read it
println!("Final account balance: {}", *balance.lock().unwrap());
}

In this code:

  • We create a Mutex to protect our shared data (here, 0).
  • Then we lock the mutex before accessing or changing the value.
  • After we dereference the locked data using *money to use or modify it.
  • When the { } block ends, the mutex unlocks automatically, so others can use it.

How To Share a Mutex Between Threads?

By default, a Mutex can only be used by one owner. But in multithreaded programs, we often want multiple threads to access and modify the same data safely.

You can use Arc (Atomic Reference Counting), which allows multiple threads to share ownership of the Mutex.

Think of Arc as a smart pointer that counts how many threads are using the data.

Example: Mutex with Threads

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
// Step 1: Create a Mutex-wrapped number and put it inside an Arc
let total = Arc::new(Mutex::new(0));

let mut threads = vec![];

// Step 2: Spawn multiple threads
for i in 1..=5 {
let total_clone = Arc::clone(&total); // share same data with each thread
let handle = thread::spawn(move || {
let mut value = total_clone.lock().unwrap(); // Lock the mutex
*value += i; // Add i to the total
println!("Thread {i} updated total to: {}", *value);
});
threads.push(handle);
}

// Step 3: Wait for all threads to finish
for handle in threads {
handle.join().unwrap();
}

// Step 4: Print final result
println!("🏁 Final total value: {}", *total.lock().unwrap());
}

Explanation:

  • Arc::new(Mutex::new(0)) wraps the value 0 in a Mutex, and then wraps that inside an Arc so it can be shared.
  • Arc::clone(&total) creates a new reference to the same Mutex for each thread.
  • lock(): Each thread locks the data before modifying it (so only one thread changes it at a time).
  • join() waits for all threads to complete before accessing the final result.

Common Errors with Mutexes

Deadlocks: A deadlock happens when two or more threads are waiting for each other to release a lock, and none of them ever does.

Example of Deadlock:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
let resource_a = Arc::new(Mutex::new(0));
let resource_b = Arc::new(Mutex::new(0));

let r1 = Arc::clone(&resource_a);
let r2 = Arc::clone(&resource_b);

// Thread 1 locks in order: A -> B
let thread1 = thread::spawn(move || {
let mut a = r1.lock().unwrap();
println!("Thread 1 locked Resource A");

let mut b = r2.lock().unwrap();
println!("Thread 1 locked Resource B");

*a += 1;
*b += 1;
});

let r1_clone = Arc::clone(&resource_a);
let r2_clone = Arc::clone(&resource_b);

// Thread 2 also locks in the same order: A -> B
let thread2 = thread::spawn(move || {
let mut a = r1_clone.lock().unwrap();
println!("Thread 2 locked Resource A");

let mut b = r2_clone.lock().unwrap();
println!("Thread 2 locked Resource B");

*a += 10;
*b += 10;
});

thread1.join().unwrap();
thread2.join().unwrap();

println!("Final values: A = {}, B = {}", *resource_a.lock().unwrap(), *resource_b.lock().unwrap());
}
  • We have two shared resources: resource_a and resource_b.
  • Both threads lock them in the same order (A → B).

Learn Other Topics About Rust

Leave a Comment