Files
rodin-bot a79249bbff fix: correct drifted citations at commit f53b654
- traits.md: iterator.rs anchor L76→L42 (pub const trait Iterator)
- traits.md: deref.rs anchor L60→L139 (pub const trait Deref)
- error-handling.md: fix variant names CliError::Io→IoError, Parse→ParseError
  to match actual stdlib doc example at convert/mod.rs:557
- concurrency.md: mutex.rs and rwlock.rs moved to sync/poison/ subtree
  (3 link updates: mutex×2, rwlock×1)
2026-05-06 17:22:48 -07:00

724 lines
20 KiB
Markdown

# 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<T>` is !Send (not thread-safe reference counting)
- `Arc<T>` is Send + Sync (atomic reference counting)
- `Cell<T>` is !Sync (interior mutability without synchronization)
- `Mutex<T>` 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<RefCell<Vec<i32>>>` 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<Mutex<T>> (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<Mutex<Vec<i32>>>) {
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<Mutex<Vec<i32>>>) {
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<Config> = 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<Option<Config>> = 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<Config> = 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::<i32>())
}).collect();
```
**Example — after:**
```rust
let data = vec![1, 2, 3, 4, 5];
let sums: Vec<i32> = thread::scope(|s| {
let handles: Vec<_> = data.chunks(2)
.map(|chunk| s.spawn(|| chunk.iter().sum::<i32>()))
.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<u32>,
}
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<Vec<u8>> = 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<Mutex<T>>)
- 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<T> (no Mutex needed)
├── Simple counter/flag → Arc<AtomicUsize> / AtomicBool
├── Read-heavy, rare writes → Arc<RwLock<T>>
└── Frequent reads and writes → Arc<Mutex<T>>
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<T>
├── Non-Copy → RefCell<T>
└── Multi-threaded → Mutex<T> or RwLock<T>
```
| Pattern | Use when |
|---|---|
| `Send + Sync` | Compiler proves thread safety |
| `Arc<Mutex<T>>` | Shared mutable state across threads |
| `Arc<RwLock<T>>` | 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
<!-- PATTERN_COMPLETE -->