| name | concurrency |
| description | Concurrency Patterns guidance for Fortress Rollback. Use when Choosing concurrency primitives, Mutex vs RwLock decisions, channel patterns. |
Concurrency Patterns
Decision Tree
Is data accessed from multiple threads?
No -> No synchronization needed
Yes -> Read-only after init?
Yes -> Arc<T>
No -> What access pattern?
Mostly reads, rare writes -> RwLock<T>
Frequent writes, short sections -> Mutex<T>
Single atomic value -> Atomic* types
Producer-consumer -> Channels
Primitive Comparison
| Primitive | Use When | Avoid When |
|---|
Mutex<T> | Short critical sections, frequent writes | Long-held locks, read-heavy |
RwLock<T> | Read-heavy, occasional writes | Write-heavy (reader starvation) |
Atomic* | Single values, lock-free algorithms | Complex multi-field updates |
Arc<T> | Shared ownership, immutable data | Mutable data (use Arc<Mutex<T>>) |
mpsc::channel | Producer-consumer, message passing | Low-latency requirements |
crossbeam::channel | High-performance channels | Simple use cases |
Key Patterns
Mutex -- Minimize Hold Time
let value = {
let guard = counter.lock();
*guard
};
expensive_operation();
RwLock -- Drop Read Before Write
let needs_write = {
let guard = self.data.read();
needs_update(&guard)
};
if needs_write {
let mut guard = self.data.write();
if needs_update(&guard) { update(&mut guard); }
}
Atomic State Machine
fn try_connect(&self) -> bool {
self.state.compare_exchange(
Disconnected as u8, Connecting as u8,
Ordering::AcqRel, Ordering::Acquire,
).is_ok()
}
Message Passing
enum Command { Process(Data), Shutdown }
fn worker(rx: mpsc::Receiver<Command>) {
loop {
match rx.recv() {
Ok(Command::Process(d)) => process(d),
Ok(Command::Shutdown) | Err(_) => break,
}
}
}
Same-Thread RAII Guards
If a Drop guard mutates thread-local state, make it explicitly !Send and !Sync
with a private PhantomData<Rc<()>> marker. Add compile_fail doctests that try
to move and borrow the guard across threads so the thread-affinity contract is
checked by doctest CI. If removing owned resources from a RefCell-backed
thread-local stack, return the removed value and drop it after releasing the
borrow so destructors can safely re-enter telemetry/logging. In guard Drop,
use LocalKey::try_with rather than with so TLS teardown cannot panic.
Memory Ordering
| Ordering | Use Case |
|---|
Relaxed | Counters where order doesn't matter |
Acquire | Load that must see prior Release stores |
Release | Store that must be visible to Acquire loads |
AcqRel | Read-modify-write (fetch_add, compare_exchange) |
SeqCst | Total ordering needed, or when in doubt |
Deadlock Prevention
- Consistent lock ordering -- always acquire in same order (by address or ID)
- Don't hold locks across await -- clone data, release, then await
- Use
try_lock for timeout-based prevention
Testing
- Loom: Deterministic exploration of all interleavings for critical code
- Stress testing: Run many iterations to catch races
- ThreadSanitizer:
RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test
Performance Tips
- Sharded locks reduce contention (e.g., 16 Mutex shards)
parking_lot is faster than std::sync (smaller Mutex, no poisoning)
- Lock-free structures via
crossbeam (SegQueue, etc.)
Common Pitfalls
| Pitfall | Fix |
|---|
No Arc for shared ownership | Arc::clone() before thread::spawn |
| Poisoned mutexes | Use parking_lot::Mutex (no poisoning) |
Rc across threads | Rc is not Send; use Arc |
| Busy-wait without yield | std::hint::spin_loop() or condvar |
std::sync::Mutex held across .await | Use tokio::sync::Mutex |
Checklist