Any multithreaded server or worker pool shares some state, and these are the standard building blocks.
To share ownership across threads you need `Arc<T>` (atomically reference counted), the thread-safe version of Rc. Cloning an Arc bumps an atomic counter; the data is read-only through it.
To mutate shared data, wrap it in a lock: `Arc<Mutex<T>>`. mutex.lock() blocks until it gets exclusive access and returns a MutexGuard, which derefs to &mut T and unlocks when dropped. There is no separate unlock call to forget; keep critical sections short by limiting the guard's scope with a block or an explicit drop(guard).
lock() returns a Result because of poisoning: if a thread panics while holding the guard, the mutex is marked poisoned, since the data might be half-updated. Most code calls .unwrap() (propagating the panic) or recovers with .unwrap_or_else(|e| e.into_inner()).
`RwLock<T>` allows many readers (read()) or one writer (write()); it helps when reads dominate and are long. For short critical sections a Mutex is often faster.
For simple counters and flags, atomics like AtomicU64 avoid locks entirely: counter.fetch_add(1, Ordering::Relaxed). Memory orderings beyond Relaxed (Acquire/Release, SeqCst) matter when the atomic guards other data; if unsure, use SeqCst or a Mutex.
Deadlocks are still possible: two threads taking locks A then B and B then A. Always acquire multiple locks in a fixed order, and never call unknown code (callbacks) while holding a lock. A guard held across a call that tries to lock the same Mutex again deadlocks the thread with itself; std mutexes are not reentrant.
Alternatives: parking_lot offers smaller, faster locks without poisoning; channels avoid shared state altogether.
Example
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::thread;
fn main() {
let totals = Arc::new(Mutex::new(Vec::new()));
let hits = Arc::new(AtomicU64::new(0));
let config = Arc::new(RwLock::new(String::from("v1")));
let handles: Vec<_> = (0..4).map(|id| {
let (totals, hits, config) = (Arc::clone(&totals), Arc::clone(&hits), Arc::clone(&config));
thread::spawn(move || {
let version = config.read().unwrap().clone(); // shared read lock
hits.fetch_add(1, Ordering::Relaxed); // lock-free counter
let mut guard = totals.lock().unwrap(); // exclusive access
guard.push(format!("{id}:{version}"));
}) // guard dropped here -> unlocked
}).collect();
for h in handles { h.join().unwrap(); }
*config.write().unwrap() = "v2".into();
let mut done = totals.lock().unwrap().clone();
done.sort();
println!("{done:?} hits={}", hits.load(Ordering::Relaxed)); // ["0:v1", "1:v1", "2:v1", "3:v1"] hits=4
}Remember
Arcshares ownership across threads;Mutex/RwLockadd mutation- Guards unlock on drop; keep them in the smallest possible scope
- Use atomics for counters and a fixed lock order to avoid deadlocks
Watch out for
- Locking a
Mutextwice on the same thread (for example in a helper called while holding the guard), which deadlocks.