SOP: Concurrency ​
Updated Mar 2026Overview ​
Rust prevents data races at compile time through the ownership system. This SOP covers threads, channels, mutexes, Arc, and the Send/Sync marker traits.
Concurrency Decision Tree ​
Spawning Threads ​
rust
use std::thread;
use std::time::Duration;
fn main() {
// Spawn a new OS thread
let handle = thread::spawn(|| {
for i in 1..=5 {
println!("Spawned thread: {i}");
thread::sleep(Duration::from_millis(100));
}
42 // Return value
});
// Main thread continues
for i in 1..=3 {
println!("Main thread: {i}");
thread::sleep(Duration::from_millis(150));
}
// Wait for spawned thread to finish
let result = handle.join().unwrap();
println!("Thread returned: {result}");
}Move Closures ​
rust
use std::thread;
fn main() {
let data = vec![1, 2, 3];
// move forces the closure to take ownership
let handle = thread::spawn(move || {
println!("Vector: {:?}", data);
// data is now owned by this thread
});
// println!("{:?}", data); // ERROR: data was moved
handle.join().unwrap();
}Channels (Message Passing) ​
rust
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
// Clone transmitter for multiple producers
let tx2 = tx.clone();
thread::spawn(move || {
tx.send("from thread 1".to_string()).unwrap();
});
thread::spawn(move || {
tx2.send("from thread 2".to_string()).unwrap();
});
// Receive all messages
for received in rx {
println!("Got: {received}");
}
}Mutex (Mutual Exclusion) ​
rust
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
// MutexGuard dropped here — lock released
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final count: {}", *counter.lock().unwrap());
// Output: Final count: 10
}RwLock (Read-Write Lock) ​
rust
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let data = Arc::new(RwLock::new(vec![1, 2, 3]));
let mut handles = vec![];
// Multiple readers can acquire simultaneously
for i in 0..3 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let read_guard = data.read().unwrap();
println!("Reader {i}: {:?}", *read_guard);
}));
}
// Writer gets exclusive access
{
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let mut write_guard = data.write().unwrap();
write_guard.push(4);
println!("Writer: added 4");
}));
}
for h in handles {
h.join().unwrap();
}
}Send and Sync ​
| Type | Send | Sync | Thread-safe alternative |
|---|---|---|---|
i32, String, Vec<T> | Yes | Yes | N/A |
Rc<T> | No | No | Arc<T> |
RefCell<T> | Yes | No | Mutex<T> or RwLock<T> |
Cell<T> | Yes | No | AtomicU32, Mutex<T> |
Arc<T> | Yes (if T: Send+Sync) | Yes (if T: Send+Sync) | N/A |
Mutex<T> | Yes (if T: Send) | Yes (if T: Send) | N/A |
| Raw pointers | No | No | Wrap in struct, impl Send/Sync |
Thread Pool Pattern ​
rust
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
type Job = Box<dyn FnOnce() + Send + 'static>;
struct ThreadPool {
workers: Vec<thread::JoinHandle<()>>,
sender: mpsc::Sender<Job>,
}
impl ThreadPool {
fn new(size: usize) -> Self {
let (sender, receiver) = mpsc::channel::<Job>();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for _ in 0..size {
let receiver = Arc::clone(&receiver);
workers.push(thread::spawn(move || loop {
let job = receiver.lock().unwrap().recv();
match job {
Ok(job) => job(),
Err(_) => break, // Channel closed
}
}));
}
ThreadPool { workers, sender }
}
fn execute<F: FnOnce() + Send + 'static>(&self, f: F) {
self.sender.send(Box::new(f)).unwrap();
}
}Deadlock Prevention ​
rust
// BAD — potential deadlock (inconsistent lock order)
fn transfer_bad(a: &Mutex<Account>, b: &Mutex<Account>, amount: f64) {
let mut a_lock = a.lock().unwrap();
let mut b_lock = b.lock().unwrap(); // Deadlock if another thread locks b then a
// ...
}
// GOOD — consistent lock order by address
fn transfer_good(a: &Mutex<Account>, b: &Mutex<Account>, amount: f64) {
let (first, second) = if std::ptr::addr_of!(*a) < std::ptr::addr_of!(*b) {
(a, b)
} else {
(b, a)
};
let mut first_lock = first.lock().unwrap();
let mut second_lock = second.lock().unwrap();
// Transfer safely
}
// GOOD — use try_lock to detect deadlocks
fn try_transfer(a: &Mutex<Account>, b: &Mutex<Account>) -> Result<(), &'static str> {
let mut a_lock = a.lock().map_err(|_| "poisoned")?;
match b.try_lock() {
Ok(mut b_lock) => { /* transfer */ Ok(()) }
Err(_) => Err("would deadlock"),
}
}Checklist ​
- [ ] Determine if you need message passing (channels) or shared state (Mutex/RwLock)
- [ ] Use
Arcfor multi-threaded shared ownership (neverRc) - [ ] Use
moveclosures when spawning threads that capture data - [ ] Keep lock scopes minimal — drop MutexGuard as soon as possible
- [ ] Lock in consistent order to prevent deadlocks
- [ ] Prefer channels over shared state when possible (simpler reasoning)
- [ ] Use
RwLockinstead ofMutexfor read-heavy workloads - [ ] Verify all types crossing thread boundaries are
Send