When it comes to sharing mutable state across multiple threads in Rust, locks are the most commonly used synchronization primitive. However, locks can introduce a lot of overhead and can lead to problems like deadlocks if not used correctly. This is where lock-free techniques come in.
Here are a few techniques you can use to safely share mutable state across multiple threads without locks:
1. Atomic Operations: Rust’s ‘std::sync::atomic‘ module provides a way to perform operations on primitive types in an atomic manner. You can use atomic operations to safely manipulate shared variables without using locks. Atomic operations are usually faster than locking approaches but are limited in what they can do.
Here’s an example where we use atomic operations to safely increment a shared counter without locks:
use std::sync::atomic::{AtomicUsize, Ordering};
fn main() {
let counter = AtomicUsize::new(0);
let mut handles = vec![];
for i in 0..10 {
let handle = std::thread::spawn(move || {
counter.fetch_add(1, Ordering::SeqCst);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(counter.load(Ordering::SeqCst), 10);
}
2. Message Passing: Another technique for sharing mutable state across multiple threads is message passing. Message passing involves sending messages between threads to communicate and synchronize access to shared state. This allows each thread to have its own copy of the shared state, and there is no need for locks or other synchronization primitives.
Here’s an example where we use message passing to share a vector across multiple threads:
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
let mut handles = vec![];
for i in 0..10 {
let tx = tx.clone();
let handle = std::thread::spawn(move || {
let mut vec = vec![i];
tx.send(vec).unwrap();
});
handles.push(handle);
}
let mut results = vec![];
for _ in 0..10 {
let vec = rx.recv().unwrap();
results.extend(vec);
}
results.sort();
assert_eq!(results, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
3. Thread-Local Storage: Thread-local storage (TLS) allows you to store a unique copy of a variable for each thread. This technique avoids the need to synchronize access to shared state altogether.
Here’s an example where we use thread-local storage to store a counter for each thread:
use std::cell::RefCell;
thread_local! {
static COUNTER: RefCell<usize> = RefCell::new(0);
}
fn main() {
let mut handles = vec![];
for i in 0..10 {
let handle = std::thread::spawn(move || {
COUNTER.with(|c| *c.borrow_mut() += 1);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
COUNTER.with(|c| assert_eq!(*c.borrow(), 1));
}
In summary, these are three powerful techniques you can use to safely share mutable state across multiple threads without locks. Depending on your use case, one technique may be more suitable than the others. Regardless of which approach you choose, always ensure that your code is race-free and free of deadlocks.