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