How it works
The explanation, with examples you can run.
Threads, shared state and channels
use std::sync::{mpsc, Arc, Mutex};use std::thread; fn main() { let hits = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..8 { let hits = Arc::clone(&hits); handles.push(thread::spawn(move || { for _ in 0..1000 { *hits.lock().unwrap() += 1; // the guard unlocks when dropped } })); } for h in handles { h.join().unwrap(); } println!("hits = {}", *hits.lock().unwrap()); let data = vec![1, 2, 3, 4]; let (left, right) = data.split_at(2); let (a, b) = thread::scope(|s| { // scoped threads may borrow local data let ha = s.spawn(|| left.iter().sum::<i32>()); let hb = s.spawn(|| right.iter().sum::<i32>()); (ha.join().unwrap(), hb.join().unwrap()) }); println!("{a} + {b}"); let (tx, rx) = mpsc::channel(); thread::spawn(move || tx.send(String::from("moved through a channel")).unwrap()); println!("{}", rx.recv().unwrap());}It prints hits = 8000 every time, then 3 + 7, then moved through a channel.
Arc<Mutex<T>>is shared, mutable state.Arcgives every thread an owner;Mutexmakes access exclusive. The data is inside the mutex, so it is impossible to touch it without locking.lock()returns a guard, and dropping the guard unlocks.unwrap()deals with poisoning: if a thread panicked while holding the lock, laterlock()calls return an error.- Scoped threads are joined before
thread::scopereturns, so they can borrowleftandrightfrom the stack withoutArcor'static. - Channels move ownership: after
send, the sending thread can't use the value any more.mpscmeans many producers, one consumer.
What the compiler stops
Move an Rc into thread::spawn and the program doesn't compile (output shortened):
error[E0277]: `Rc<i32>` cannot be sent between threads safely = help: within `{closure@src/main.rs:5:27: 5:34}`, the trait `Send` is not implemented for `Rc<i32>`note: required by a bound in `spawn` | F: Send + 'static,The same mechanism rejects sharing a RefCell between threads (not Sync), mutating a captured variable from two threads without a lock, and moving a MutexGuard to another thread (guards aren't Send, because the underlying system locks must be released by the thread that acquired them).
What it doesn't stop
Data-race freedom isn't freedom from all concurrency bugs. Deadlocks, lock-ordering mistakes, starvation and higher-level races (read, decide, then write under two separate locks) are still possible. Keep critical sections short, take locks in a consistent order, and prefer message passing or owning data per thread when the design allows.
Your own types get Send and Sync automatically when all their fields have them. Implementing them by hand is unsafe impl Send for MyType {}: a promise the compiler can't check, needed only for types built on raw pointers.
For heavier data parallelism, crates such as rayon turn iter() into par_iter() and rely on the same Send/Sync bounds to stay safe. std::sync::atomic provides lock-free counters and flags, with explicit memory orderings.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.