Shared memory
Starting off with the oldest and simplest method of shared memory, a disclaimer first: You shouldn't use shared memory unless you have a special reason for it. Other methods can be much more ergonomic and harder to mess up. That said, it is supported by the most languages, particulary lower level ones.
When sharing memory between threads, Rust places additional restrictions on how to access it, controlled by the Send
and Sync
traits on types.
Thanks to Rusts trait system, you can't use anything that would break synchronisation, because it is missing one or both of those traits. Let's look at the most important examples:
Rc
is a wrapper enabling shared ownership of data. Doesn't work across threads, there's another version calledArc
for it. It has slightly higher overhead due to the synchronisation.Cell
andRefCell
provide means to mutate things that are normally immutable, but aren't thread safe. Thread save alternatives areRwLock
andMutex
.
Thinking a bit about Rusts ownership model
in combination with the Sync
trait, we realise that we can immutably borrow something to another thread, but mutable borrows are out of the question. (Even immutable borrows would have lifetimes problems, but we will ignore them for now.)
Arc
functions the same way: Its content can be read from multiple threads, but there is no way to get a mutable reference to the inner value.
The reason is the same as for the general borrowing rules, but in addition, a value with mutable borrows could change at any time for the observing thread, between any two instructions.
This could cause all sorts of erroneous behaviour, so all the more reason to prohibit it. But then, how could we make changes to a value that can be observed by another thread?
The standard library delivers a few useful types for this use case. Atomic types
provide methods that combine the read and write step, so no thread can interfere. For this reason, those methods don't actually require the type to be mutable.
The other important method is to provide exclusivity at runtime, blocking other threads from reading a value that is currently borrowed mutably. This is exactly what RwLock
and Mutex
do.
To complete the overview of shared memory in Rust, an example:
#![allow(unused)] fn main() { use std::sync::{Arc, Mutex}; let mut threads = Vec::new(); // Could also use an atomic value for pure counting, // but we want side effects! let counter: Arc<Mutex<usize>> = Arc::default(); for i in 0..20 { let thread_counter = counter.clone(); threads.push(std::thread::spawn(move || { // Only one thread can be inside here at a time if let Ok(mut counter) = thread_counter.lock() { println!("Thread {} counting!", i); *counter += 1; } })); } // Wait for all threads to finish for thread in threads { thread.join().unwrap(); } // 100 println!("Counted to {}!", *counter.lock().unwrap()); }