docs: concurrency patterns from rust-lang/rust
10 patterns, 723 lines. Full spec compliance. Patterns: Send/Sync, Arc<Mutex<T>>, RwLock, atomics, channels, OnceLock/LazyLock, scoped threads, Cell/RefCell, thread_local!, poisoning.
This commit is contained in:
@@ -0,0 +1,723 @@
|
||||
# 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/mutex.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/sync/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/rwlock.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/sync/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/mutex.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/sync/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 -->
|
||||
Reference in New Issue
Block a user