# Rust Concurrency Patterns Patterns for concurrent programming in Rust, extracted from the standard library source. **Source:** [rust-lang/rust](https://github.com/rust-lang/rust) at commit [`f53b654`](https://github.com/rust-lang/rust/tree/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6) **Stats:** 783 Atomic type references, 721 Ordering usages, 274 Send/Sync markers, 135 Mutex/RwLock, 125 channel references, 254 OnceLock/LazyLock usages. --- ## 1. Send + Sync (Compiler-Enforced Thread Safety) ### Source: [library/core/src/marker.rs#L92](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/core/src/marker.rs#L92) (Send), [library/core/src/marker.rs#L657](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/core/src/marker.rs#L657) (Sync) ```rust pub unsafe auto trait Send { } pub unsafe auto trait Sync { } ``` ### Why Send and Sync are auto traits — the compiler implements them automatically when safe. Send means "can be moved to another thread." Sync means "can be shared between threads via &T." These are the foundation of Rust's fearless concurrency: data races are compile-time errors, not runtime bugs. - `Rc` is !Send (not thread-safe reference counting) - `Arc` is Send + Sync (atomic reference counting) - `Cell` is !Sync (interior mutability without synchronization) - `Mutex` is Sync (provides synchronized access) ### When to Use **Triggers:** - When your type contains raw pointers that ARE safe to send (implement `unsafe impl Send`) - When you need to prove thread safety at compile time (bound with `T: Send + Sync`) - When designing concurrent APIs (require Send on closures for spawn) **Example — before:** ```rust // Compiles but has a data race (other languages): let data = Rc::new(RefCell::new(vec![1, 2, 3])); let data_clone = data.clone(); thread::spawn(move || { data_clone.borrow_mut().push(4); // RACE CONDITION }); data.borrow_mut().push(5); ``` **Example — in Rust:** ```rust // COMPILE ERROR: `Rc>>` cannot be sent between threads safely let data = Rc::new(RefCell::new(vec![1, 2, 3])); thread::spawn(move || { data.borrow_mut().push(4); // ERROR: Rc is !Send }); // FIX: Use Arc + Mutex for thread-safe shared mutation let data = Arc::new(Mutex::new(vec![1, 2, 3])); let data_clone = Arc::clone(&data); thread::spawn(move || { data_clone.lock().unwrap().push(4); // safe: Mutex provides synchronization }); ``` ### When NOT to Use **Don't use this when:** - Your type is inherently single-threaded (let the auto trait do its job — it will be !Send automatically if it contains !Send fields) - You're unsure whether your type is safe to send (DON'T blindly add `unsafe impl Send` — prove it first) --- ## 2. Arc> (Shared Mutable State) ### Source: [library/std/src/sync/poison/mutex.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/sync/poison/mutex.rs), [library/alloc/src/sync.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/alloc/src/sync.rs) ```rust use std::sync::{Arc, Mutex}; let counter = Arc::new(Mutex::new(0)); let counter_clone = Arc::clone(&counter); thread::spawn(move || { let mut num = counter_clone.lock().unwrap(); *num += 1; }); ``` ### Why Arc provides shared ownership across threads. Mutex provides exclusive access (one thread at a time). Together: multiple threads can share mutable state safely. The lock guard (MutexGuard) drops automatically, releasing the lock — you can't forget to unlock. ### When to Use **Triggers:** - Multiple threads need to read AND write the same data - Writes are short-lived (don't hold the lock long) - You need strong consistency (every read sees the latest write) **Example — before:** ```rust // Single-threaded: RefCell is fine let state = RefCell::new(HashMap::new()); state.borrow_mut().insert("key", "value"); ``` **Example — after (multi-threaded):** ```rust use std::sync::{Arc, Mutex}; let state = Arc::new(Mutex::new(HashMap::new())); // Thread 1: let state1 = Arc::clone(&state); thread::spawn(move || { state1.lock().unwrap().insert("key", "value"); }); // Thread 2: let state2 = Arc::clone(&state); thread::spawn(move || { let map = state2.lock().unwrap(); println!("{:?}", map.get("key")); }); ``` ### When NOT to Use **Don't use this when:** - Only one thread needs access (just own it directly) - Reads vastly outnumber writes (use `RwLock` instead) - Lock contention is high (consider lock-free data structures or message passing) - The protected data is a simple counter (use `AtomicUsize`) ### Anti-pattern ```rust // DON'T: Hold the lock across await points let data = Arc::new(Mutex::new(vec![])); async fn bad(data: Arc>>) { let mut guard = data.lock().unwrap(); some_async_operation().await; // DEADLOCK RISK: lock held across await guard.push(1); } // DO: Lock, extract, drop, then await async fn good(data: Arc>>) { let value = { let guard = data.lock().unwrap(); guard.last().copied() }; // lock dropped here some_async_operation().await; // safe: no lock held } ``` --- ## 3. RwLock for Read-Heavy Workloads ### Source: [library/std/src/sync/poison/rwlock.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/sync/poison/rwlock.rs) ```rust use std::sync::RwLock; let lock = RwLock::new(5); // Multiple readers simultaneously: let r1 = lock.read().unwrap(); let r2 = lock.read().unwrap(); assert_eq!(*r1, 5); assert_eq!(*r2, 5); drop((r1, r2)); // Exclusive writer: let mut w = lock.write().unwrap(); *w += 1; ``` ### Why RwLock allows multiple simultaneous readers OR one exclusive writer. For read-heavy workloads, this dramatically reduces contention compared to Mutex (which blocks all access during any operation). ### When to Use **Triggers:** - Reads vastly outnumber writes (>10:1 ratio) - Read operations are not trivially fast (worth avoiding the lock) - Multiple threads frequently read simultaneously ### When NOT to Use **Don't use this when:** - Writes are frequent (RwLock has higher overhead than Mutex) - You're on a single-threaded runtime - The protected operation is so fast that contention doesn't matter --- ## 4. Atomic Types for Lock-Free Operations ### Source: [library/core/src/sync/atomic.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/core/src/sync/atomic.rs) 783 Atomic type references, 721 Ordering usages. ```rust use std::sync::atomic::{AtomicUsize, Ordering}; static COUNTER: AtomicUsize = AtomicUsize::new(0); fn increment() { COUNTER.fetch_add(1, Ordering::Relaxed); } ``` ### Why Atomics provide thread-safe operations WITHOUT locks. They're faster than Mutex for simple values (counters, flags, pointers) because they use CPU hardware instructions directly. ### When to Use **Triggers:** - Simple values: counters, flags, booleans - High contention where lock overhead matters - Statistics/metrics that don't need precise ordering - Implementing lock-free data structures **Example — before:** ```rust // Mutex for a simple counter — overkill let counter = Arc::new(Mutex::new(0usize)); let c = Arc::clone(&counter); thread::spawn(move || { *c.lock().unwrap() += 1; // lock overhead for one add }); ``` **Example — after:** ```rust use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; let counter = Arc::new(AtomicUsize::new(0)); let c = Arc::clone(&counter); thread::spawn(move || { c.fetch_add(1, Ordering::Relaxed); // no lock, hardware instruction }); ``` ### When NOT to Use **Don't use this when:** - You need to update multiple values atomically (use Mutex) - The operation is complex (compare-and-swap loops get tricky) - You don't understand memory ordering (use Mutex instead — wrong Ordering is UB in theory, bugs in practice) ### Ordering Guide ```rust // Simple counter — order doesn't matter: counter.fetch_add(1, Ordering::Relaxed); // Flag that another thread checks: flag.store(true, Ordering::Release); // writer if flag.load(Ordering::Acquire) { ... } // reader // When in doubt (and you can afford it): value.load(Ordering::SeqCst); // strongest, safest, slowest ``` --- ## 5. Channels (mpsc) for Message Passing ### Source: [library/std/src/sync/mpsc.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/sync/mpsc.rs) 125 channel references in library/. ```rust use std::sync::mpsc; let (tx, rx) = mpsc::channel(); thread::spawn(move || { tx.send("hello").unwrap(); }); let msg = rx.recv().unwrap(); ``` ### Why Channels move data between threads without shared state. "Do not communicate by sharing memory; share memory by communicating." The sender and receiver are separate types — the type system ensures correct usage. ### When to Use **Triggers:** - Producer-consumer patterns - Work distribution (fan-out) - Event notification between threads - You want to avoid shared mutable state entirely **Example — before:** ```rust // Shared state approach — complex and error-prone let results = Arc::new(Mutex::new(Vec::new())); for i in 0..10 { let results = Arc::clone(&results); thread::spawn(move || { let answer = compute(i); results.lock().unwrap().push(answer); }); } // How do we know when all threads are done? ``` **Example — after:** ```rust let (tx, rx) = mpsc::channel(); for i in 0..10 { let tx = tx.clone(); thread::spawn(move || { let answer = compute(i); tx.send(answer).unwrap(); }); } drop(tx); // close the sender so rx.iter() terminates let results: Vec<_> = rx.iter().collect(); // Clean, simple, no shared state ``` ### When NOT to Use **Don't use this when:** - You need multiple consumers (mpsc = multi-producer, SINGLE consumer) - You need bidirectional communication (use two channels or shared state) - The "message" is just a counter increment (use atomics) --- ## 6. OnceLock / LazyLock for One-Time Initialization ### Source: [library/std/src/sync/once_lock.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/sync/once_lock.rs) 254 OnceLock/LazyLock references. ```rust use std::sync::OnceLock; static CONFIG: OnceLock = OnceLock::new(); fn get_config() -> &'static Config { CONFIG.get_or_init(|| { load_config_from_disk() // runs exactly once, thread-safe }) } ``` ### Why OnceLock guarantees initialization happens exactly once, even with concurrent access. Multiple threads calling `get_or_init` simultaneously will block until the first one finishes, then all get the same value. ### When to Use **Triggers:** - Global/static configuration loaded once at startup - Expensive computation cached forever - Regex compilation (compile once, use many times) - Database connection pools initialized on first use **Example — before:** ```rust // Mutex just for initialization — unnecessary lock on every access static CONFIG: Mutex> = Mutex::new(None); fn get_config() -> Config { let mut config = CONFIG.lock().unwrap(); if config.is_none() { *config = Some(load_config()); } config.as_ref().unwrap().clone() // clone on EVERY access! } ``` **Example — after:** ```rust use std::sync::LazyLock; static CONFIG: LazyLock = LazyLock::new(|| load_config()); fn get_config() -> &'static Config { &CONFIG // no lock after initialization, just a pointer read } ``` ### When NOT to Use **Don't use this when:** - The value needs to change after initialization (use Mutex/RwLock) - Initialization failure should be retried (OnceLock panics in init leave it permanently empty) - You need non-static lifetime (use regular lazy initialization) --- ## 7. Scoped Threads (thread::scope) ### Source: [library/std/src/thread/scoped.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/thread/scoped.rs) ```rust let mut data = vec![1, 2, 3]; thread::scope(|s| { s.spawn(|| { println!("{data:?}"); // can borrow from parent scope! }); s.spawn(|| { println!("{}", data.len()); }); }); // All threads joined here — data is safe to use again ``` ### Why Regular `thread::spawn` requires 'static data (because the thread might outlive the caller). `thread::scope` guarantees all spawned threads finish before the scope ends, so they can BORROW local data. No Arc needed. ### When to Use **Triggers:** - Parallel processing of local data - Fork-join parallelism - You want multiple threads to borrow the same data without Arc **Example — before:** ```rust // thread::spawn requires ownership or 'static — must Arc everything let data = Arc::new(vec![1, 2, 3, 4, 5]); let handles: Vec<_> = data.chunks(2).map(|chunk| { // Can't do this — chunks borrows data which isn't 'static thread::spawn(move || chunk.iter().sum::()) }).collect(); ``` **Example — after:** ```rust let data = vec![1, 2, 3, 4, 5]; let sums: Vec = thread::scope(|s| { let handles: Vec<_> = data.chunks(2) .map(|chunk| s.spawn(|| chunk.iter().sum::())) .collect(); handles.into_iter().map(|h| h.join().unwrap()).collect() }); // No Arc, no clone, just direct borrows ``` ### When NOT to Use **Don't use this when:** - Threads need to outlive the current function (use regular spawn) - You're in an async context (use async tasks instead) - The work is too small to benefit from parallelism --- ## 8. Interior Mutability (Cell/RefCell) ### Source: [library/core/src/cell.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/core/src/cell.rs) 410 Cell/RefCell usages in library/. ```rust use std::cell::Cell; struct Counter { count: Cell, } impl Counter { fn increment(&self) { // note: &self, not &mut self self.count.set(self.count.get() + 1); } } ``` ### Why Sometimes you need mutation through a shared reference (&T). Cell provides this for Copy types (get/set), RefCell for any type (runtime borrow checking). This is !Sync — single-thread only. ### When to Use **Triggers:** - You need mutation through &self (caching, counters, lazy init) - Multiple parts of code hold &T but one needs to mutate - Single-threaded context only - **Cell:** Value is Copy (integers, booleans) - **RefCell:** Value isn't Copy or you need a reference into it **Example — before:** ```rust // Can't cache in &self — need &mut self for mutation impl Graph { fn expensive_traversal(&mut self) -> &[Node] { if self.cache.is_none() { self.cache = Some(self.compute_traversal()); } self.cache.as_ref().unwrap() } } // Every caller needs &mut Graph just to read cached data ``` **Example — after:** ```rust use std::cell::RefCell; impl Graph { fn expensive_traversal(&self) -> Ref<'_, [Node]> { if self.cache.borrow().is_none() { *self.cache.borrow_mut() = Some(self.compute_traversal()); } Ref::map(self.cache.borrow(), |c| c.as_ref().unwrap().as_slice()) } } // Callers only need &Graph — mutation is interior ``` ### When NOT to Use **Don't use this when:** - You can restructure to use &mut self (preferred) - Multi-threaded (use Mutex/RwLock instead — Cell/RefCell are !Sync) - RefCell borrow violations would be common (panic at runtime) --- ## 9. thread_local! for Per-Thread State ### Source: [library/std/src/thread/local.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/thread/local.rs) ```rust thread_local! { static BUFFER: RefCell> = RefCell::new(Vec::new()); } fn process(data: &[u8]) { BUFFER.with(|buf| { let mut buf = buf.borrow_mut(); buf.clear(); buf.extend_from_slice(data); // reuses allocation across calls within same thread }); } ``` ### Why Thread-local storage gives each thread its own copy. No synchronization needed because there's no sharing. Useful for per-thread caches, buffers, and RNG state. ### When to Use **Triggers:** - Per-thread caches or buffers (avoid allocation per call) - Thread-specific state (random number generators) - Avoiding synchronization overhead for thread-independent data ### When NOT to Use **Don't use this when:** - State needs to be shared between threads (use Arc>) - You're in an async context (tasks migrate between threads — thread-local doesn't follow the task) - The state is small and cheap to create per-call --- ## 10. Poisoning (Mutex Recovery After Panic) ### Source: [library/std/src/sync/poison/mutex.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/sync/poison/mutex.rs) ```rust let mutex = Arc::new(Mutex::new(0)); // Thread panics while holding the lock: let m = Arc::clone(&mutex); let _ = thread::spawn(move || { let _guard = m.lock().unwrap(); panic!("oops"); }).join(); // The mutex is now "poisoned" — lock() returns Err: match mutex.lock() { Ok(guard) => println!("value: {}", *guard), Err(poisoned) => { // Can still access the data if you choose: let guard = poisoned.into_inner(); println!("recovered: {}", *guard); } } ``` ### Why If a thread panics while holding a Mutex, the protected data may be in an inconsistent state. Rust "poisons" the mutex: future lock() calls return Err, alerting other threads that something went wrong. You can choose to recover or propagate the panic. ### When to Use **Triggers:** - You need to handle the case where a sibling thread panicked - You want to "unwrap" the poisoned lock when you know the data is still valid - Robust services that shouldn't crash because one request panicked ### When NOT to Use **Don't use this when:** - A panic in one thread should crash everything (just `.unwrap()`) - You're using `catch_unwind` to prevent panics from poisoning - The data is always valid regardless of panic point --- ## Summary: Concurrency Decision Tree ``` Do you need shared state across threads? ├── NO → Use owned data + message passing (channels) └── YES → What kind of access? ├── Read-only → Arc (no Mutex needed) ├── Simple counter/flag → Arc / AtomicBool ├── Read-heavy, rare writes → Arc> └── Frequent reads and writes → Arc> Do threads need to borrow local data? ├── YES → thread::scope (no Arc needed) └── NO → thread::spawn (requires Send + 'static) Need one-time initialization? ├── Static, known at compile time → const ├── Static, computed at runtime → LazyLock / OnceLock └── Per-instance → Option + init-on-first-use Single-threaded interior mutability? ├── Copy type → Cell ├── Non-Copy → RefCell └── Multi-threaded → Mutex or RwLock ``` | Pattern | Use when | |---|---| | `Send + Sync` | Compiler proves thread safety | | `Arc>` | Shared mutable state across threads | | `Arc>` | Read-heavy shared state | | `AtomicUsize` etc. | Lock-free counters/flags | | `mpsc::channel` | Message passing between threads | | `OnceLock`/`LazyLock` | One-time initialization | | `thread::scope` | Parallel work on local data | | `Cell`/`RefCell` | Interior mutability (single-thread) | | `thread_local!` | Per-thread state | | Poisoning | Recovery after panic | See also: - [ownership.md](ownership.md) — Arc vs Rc, Box - [unsafe-patterns.md](unsafe-patterns.md) — unsafe impl Send/Sync - [error-handling.md](error-handling.md) — Handling poisoned locks