| name | bos-concurrency-rust |
| description | Write Rust code in the style of Mara Bos, Rust library team lead and author of "Rust Atomics and Locks." Emphasizes low-level concurrency, atomics, and understanding the memory model. Use when writing concurrent or lock-free code. |
Mara Bos Style Guide
Overview
Mara Bos is the Rust library team lead and author of "Rust Atomics and Locks." She maintains core synchronization primitives in the standard library. Her expertise: making concurrent code correct, efficient, and understandable.
Core Philosophy
"Concurrency bugs are hard to find. Make them impossible instead."
"Understand the memory model before using atomics."
Bos believes that concurrent code must be provably correct. Understanding happens-before relationships and memory ordering is essential, not optional.
Design Principles
-
Correctness First: A fast but incorrect concurrent algorithm is worthless.
-
Understand Ordering: Every atomic operation needs the right memory ordering.
-
Minimize Shared State: Less sharing means fewer bugs.
-
Prefer High-Level Abstractions: Use channels and mutexes before atomics.
When Writing Code
Always
- Use the highest-level abstraction that works (channels > mutexes > atomics)
- Document the synchronization strategy for concurrent code
- Test concurrent code with tools like Miri and loom
- Understand why each memory ordering is chosen
- Consider what happens if operations interleave
Never
- Use
Ordering::Relaxed without understanding the implications
- Assume operations happen in source code order
- Write lock-free code without formal reasoning
- Ignore potential data races in unsafe code
Prefer
Mutex<T> over manual locking
crossbeam channels over std::sync::mpsc
parking_lot for high-performance locking
Ordering::SeqCst when unsure (then optimize if needed)
Code Patterns
The Ordering Hierarchy
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
fn increment() {
COUNTER.fetch_add(1, Ordering::Relaxed);
}
static READY: AtomicBool = AtomicBool::new(false);
static mut DATA: u64 = 0;
fn producer() {
unsafe { DATA = 42; }
READY.store(true, Ordering::Release);
}
fn consumer() {
while !READY.load(Ordering::Acquire) {}
unsafe { println!("{}", DATA); }
}
static FLAG_A: AtomicBool = AtomicBool::new(false);
static FLAG_B: AtomicBool = AtomicBool::new(false);
Implementing a Spinlock
use std::sync::atomic::{AtomicBool, Ordering};
use std::cell::UnsafeCell;
use std::ops::{Deref, DerefMut};
pub struct SpinLock<T> {
locked: AtomicBool,
data: UnsafeCell<T>,
}
unsafe impl<T: Send> Send for SpinLock<T> {}
unsafe impl<T: Send> Sync for SpinLock<T> {}
impl<T> SpinLock<T> {
pub const fn new(data: T) -> Self {
SpinLock {
locked: AtomicBool::new(false),
data: UnsafeCell::new(data),
}
}
pub fn lock(&self) -> SpinLockGuard<'_, T> {
while self.locked
.compare_exchange_weak(
false,
true,
Ordering::Acquire,
Ordering::Relaxed,
)
.()
{
std::hint::();
}
SpinLockGuard { lock: }
}
}
<, T> {
lock: & SpinLock<T>,
}
<T> Deref <, T> {
= T;
(&) &T {
{ &*.lock.data.() }
}
}
<T> DerefMut <, T> {
(& ) & T {
{ & *.lock.data.() }
}
}
<T> <, T> {
(& ) {
.lock.locked.(, Ordering::Release);
}
}
Arc and Weak for Shared Ownership
use std::sync::{Arc, Weak};
struct Node {
value: i32,
children: Vec<Arc<Node>>,
parent: Weak<Node>,
}
fn create_tree() -> Arc<Node> {
let root = Arc::new(Node {
value: 1,
children: Vec::new(),
parent: Weak::new(),
});
let child = Arc::new(Node {
value: 2,
children: Vec::new(),
parent: Arc::downgrade(&root),
});
root
}
fn traverse_up(node: &Node) {
if let Some(parent) = node.parent.upgrade() {
println!("Parent value: {}", parent.value);
traverse_up(&parent);
}
}
Channel Patterns
use std::sync::mpsc;
use std::thread;
fn producer_consumer() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
for i in 0..10 {
tx.send(i).unwrap();
}
});
for received in rx {
println!("Got: {}", received);
}
}
fn multi_producer() {
let (tx, rx) = mpsc::channel();
for i in 0..4 {
let tx_clone = tx.clone();
thread::spawn(move || {
tx_clone.send(format!("from thread {}", i)).unwrap();
});
}
drop(tx);
for msg in rx {
(, msg);
}
}
() {
(tx, rx) = mpsc::();
thread::( || {
.. {
tx.(i).();
}
});
}
Testing Concurrent Code
#[cfg(test)]
mod tests {
use loom::sync::atomic::{AtomicUsize, Ordering};
use loom::thread;
#[test]
fn test_concurrent_increment() {
loom::model(|| {
let counter = AtomicUsize::new(0);
let counter1 = &counter;
let counter2 = &counter;
let t1 = thread::spawn(move || {
counter1.fetch_add(1, Ordering::SeqCst);
});
let t2 = thread::spawn(move || {
counter2.fetch_add(1, Ordering::SeqCst);
});
t1.join().unwrap();
t2.join().unwrap();
assert_eq!(counter.load(Ordering::SeqCst), 2);
});
}
}
Mental Model
Bos thinks about concurrency as:
- What is shared? Identify all shared state.
- What orderings can occur? Consider all interleavings.
- What synchronization is needed? Ensure happens-before.
- Can I prove correctness? If not, simplify.
Memory Ordering Cheat Sheet
| Ordering | Use Case |
|---|
Relaxed | Counters, statistics (no sync needed) |
Acquire | Load that precedes accessing protected data |
Release | Store that follows modifying protected data |
AcqRel | Read-modify-write that does both |
SeqCst | When you need global ordering (default choice) |