Rust by Example: Shared State Concurrency
Combine Arc and Mutex to share mutable state across threads. This example demonstrates the standard pattern for safe shared-state concurrency, ensuring data consistency in a multi-threaded environment.
Code
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// We want to share a counter between 10 threads.
// Mutex provides interior mutability.
// Arc provides shared ownership.
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
// Clone the Arc to get a new reference to the SAME mutex
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
// Lock the mutex to get access to the integer
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
// Wait for all threads to finish
for handle in handles {
handle.join().unwrap();
}
// Acquire the lock one last time to print the result
println!("Result: {}", *counter.lock().unwrap());
}Explanation
While message passing (channels) is often preferred, sometimes it's more natural to share state directly. In Rust, sharing mutable state across threads requires two components: 1) A way to ensure exclusive access (Mutex), and 2) A way to have multiple owners of the data (Arc).
Arc<T> (Atomic Reference Counted) is a thread-safe version of Rc<T>. It allows multiple threads to own the same data. However, Arc only gives immutable access. To modify the data, we wrap it in a Mutex<T>. This combination, Arc<Mutex<T>>, is the standard pattern for shared mutable state in Rust.
The Rust compiler enforces thread safety at compile time. If you tried to use Rc instead of Arc, or RefCell instead of Mutex, the code would not compile because those types do not implement the Send and Sync traits required for safe concurrent access.
Code Breakdown
Arc::new(Mutex::new(0)). This creates the shared memory structure. The integer 0 is protected by the Mutex, which is managed by the Arc.Arc::clone(&counter). Increments the atomic reference count. We do this for each thread so that each thread has its own "handle" to the shared data. This is a cheap operation.counter.lock().unwrap(). This line blocks the thread until the lock is acquired. It returns a MutexGuard which dereferences to the inner integer (&mut i32).*num += 1. Because MutexGuard implements DerefMut, we can treat num like a mutable reference to the integer. The lock is released automatically when num goes out of scope at the end of the closure.
