While message passing is a great way to handle concurrency, there are times when you need multiple threads to access the same shared data. This is known as shared-state concurrency. However, it comes with risks, such as data races, where multiple threads try to read and write to the same memory location at the same time, leading to corrupted data.
Rust’s type system and ownership rules provide tools to manage shared state safely. The primary tool for this is the Mutex<T>.
Mutual Exclusion with Mutex<T>
A Mutex (short for “mutual exclusion”) is a concurrency primitive that ensures only one thread can access some data at any given time. To access the data, a thread must first signal that it wants access by acquiring the mutex’s “lock.” The lock is a data structure that is part of the mutex and keeps track of who has exclusive access. Once the thread is done, it “unlocks” the data, allowing another thread to acquire the lock.
In Rust, this is handled by the std::sync::Mutex<T> struct.
Let’s see how it works:
- You wrap your shared data in a
Mutex<T>. - Before using the data, you call the
lock()method on the mutex. - The
lock()method blocks the current thread until it’s able to acquire the lock. It returns aResult. If another thread that held the lock panicked,lock()will return an error. - If successful,
lock()returns a smart pointer calledMutexGuard. This smart pointer implementsDerefto let you access the inner data and, crucially,Drop. - When the
MutexGuardgoes out of scope, itsdropimplementation automatically releases the lock.
This RAII (Resource Acquisition Is Initialization) pattern makes it impossible to forget to release the lock, which is a common bug in other languages.
Sharing a Mutex<T> Between Threads
If you try to pass a Mutex<T> to multiple threads, you’ll run into a problem with the ownership rules. The first thread you pass it to will take ownership.
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
// This code will not compile!
thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
}
To allow multiple threads to “own” the mutex, we need to combine it with another smart pointer: Arc<T>.
Arc<T>: Atomic Reference Counting
Arc<T> stands for Atomically Reference Counted. It’s a thread-safe version of Rc<T>. It lets multiple threads share ownership of a value, and it guarantees that the value will only be deallocated when the last reference to it is dropped. The reference counting is done using atomic operations, which makes it safe to use across threads.
The standard pattern for sharing mutable state in Rust is Arc<Mutex<T>>.
Let’s fix our counter example using this pattern.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Wrap the Mutex in an Arc to allow shared ownership across threads.
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
// Clone the Arc for each thread. This increases the reference count.
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// Each thread locks the mutex to get access to the data.
let mut num = counter.lock().unwrap();
*num += 1;
// The lock is automatically released when `num` goes out of scope.
});
handles.push(handle);
}
// Wait for all the threads to finish.
for handle in handles {
handle.join().unwrap();
}
// At the end, we can lock the mutex and print the final value.
println!("Result: {}", *counter.lock().unwrap());
}
In this correct example:
- The
Mutexis wrapped in anArc. - For each new thread, we
clonetheArc. This creates a new pointer to the sameMutexand increments the atomic reference count. - Each thread can now safely acquire the lock, mutate the data, and release the lock.
- The
mainthread waits for all threads to complete. - The final result will be
10, as expected.
Conclusion
Sharing state between threads can be complex, but Rust provides powerful and safe tools to manage it. The Arc<Mutex<T>> pattern is the idiomatic way to allow multiple threads to read and write to shared data. By leveraging the type system and smart pointers, Rust helps you avoid common concurrency pitfalls like data races and forgetting to release locks, enabling “fearless concurrency.”