Arc<Mutex<_>> in Rust
A quick example of how to share data across threads in Rust using a Mutex
. First, a quick explanation:
Our data (my_data
, below) is shared between multiple threads – one thread might be updating it, another might want to read from it, yet another might simultaneously try to update it from another entrypoint. We only have one copy of the data, so we use mutual exclusion to allow exactly one entity to gain access to it at a time. But the mutex itself can’t be safely accessed across multiple, so we use Arc
– atomically reference counted – to proctor access to the mutex.
Thus we have one copy of my_data
, one Mutex wrapping it, and any number of fairly inexpensive Arcs*. The entire code is here, and some brief explanation below:
use std::thread;
use std::{
sync::{Arc, Mutex},
time::Duration,
};
fn main() {
// create my data, wrap it in a mutex, then add atomic reference couting
let my_data = vec![1, 2, 3];
let my_data = Mutex::new(my_data);
let my_data = Arc::new(my_data);
// spawn a thread that will update the values
// a clone of our Arc will be moved into the thread
let thread_arc = my_data.clone();
let t1 = thread::spawn(move || {
println!("Thread 1 attempting to acquire lock...");
if let Ok(mut x) = thread_arc.lock() {
println!("Thread 1 acquired lock");
for num in x.iter_mut() {
*num += 1;
}
// simulate some long-running work
thread::sleep(Duration::from_millis(750));
};
println!("Thread 1 dropped lock");
// Do something else with the data
thread::sleep(Duration::from_millis(900));
});
let thread_arc = my_data.clone();
let t2 = thread::spawn(move || {
println!("Thread 2 attempting to acquire lock...");
if let Ok(mut x) = thread_arc.lock() {
println!("Thread 2 acquired lock");
for num in x.iter_mut() {
*num *= 2;
}
// simulate some long-running work
thread::sleep(Duration::from_millis(1250));
};
println!("Thread 2 dropped lock");
// Do something else with the data
thread::sleep(Duration::from_millis(1100));
});
t1.join().unwrap();
t2.join().unwrap();
let my_data = my_data.lock().unwrap();
println!("Values are: {:?}", my_data);
}
Now for some explanation. First, we create data. Then we can shadow it for simplicity, wrapping the data first in a Mutex
, then in an Arc
. This can be done in one expression; here, I do it for clarity:
let my_data = vec![1, 2, 3];
let my_data = Mutex::new(my_data);
let my_data = Arc::new(my_data);
Next, we spawn two threads. Examining the first one, we first clone the Arc<Mutex<Vec<i32>>>
:
// spawn a thread that will update the values
// a clone of our Arc will be moved into the thread
let thread_arc = my_data.clone();
To be clear, this clones the Arc
; this gives us a reference to one and only Mutex
that wraps our one and only vector of data. When we spawn a new thread, we explicitly move
this newly-cloned Arc
into the thread. thread_arc
is now no longer available in our main thread.
This new thread starts running by immediately attempting to acquire a lock on our data. It may or may not get it right away. If it does, then it will start modifying data; if not, it will block until it has access. Only one thread is able to access our data at a time.
let t1 = thread::spawn(move || {
println!("Thread 1 attempting to acquire lock...");
if let Ok(mut x) = thread_arc.lock() {
println!("Thread 1 acquired lock");
for num in x.iter_mut() {
*num += 1;
}
// simulate some long-running work
thread::sleep(Duration::from_millis(750));
};
println!("Thread 1 dropped lock");
// Do something else with the data
thread::sleep(Duration::from_millis(900));
});
When both threads are running, the main thread calls .join
to wait for them to finish. Here, we don’t really care about the order in which .join
is called becuase we don’t want to do anything else until both have completed. These are blocking operations, so our main thread will wait until they’re done. At that point, we acquire the lock, unwrap its Result
, and we have the updated data. If we didn’t call .join
, our main thread would likely acquire the lock before the threads had time to start, and the program would exit before the threads had a chance to run.
t1.join().unwrap();
t2.join().unwrap();
let my_data = my_data.lock().unwrap();
One other note is that our two threads are basically started at the same time, so which one gets executed first is indeterminate. Therefore, our initial values of [1, 2, 3]
might end up as [3, 5, 7]
or [4, 6, 8]
.
* - Per the Arc
documentation, “atomic operations are more expensive than ordinary memory accesses (Rc
)”, but using Arc
overall isn’t terribly expensive.
Update: unreasonable simplicity
The original code I posted above was unreasonably simple: both threads acquired a lock, simulated work, and returned. This meant that both threads essentially ran one after the other but not truly simultaneously.
Before and/or after the critical section (within the if let
block), we presumably want to do something else. In a real project, maybe it’s preparing some data to insert into a shared data structure before we acquire it. Maybe we update a value and calculate something from it afterward. But the critical section shouldn’t be the only thing in the thread, otherwise you have one thread using the mutex and all other threads waiting for their turn, one by one.
If your code is only doing something inside the mutex’s critical section, then multithreading probably isn’t the way to go.