Synchronization Primitives
Choosing Synchronization Primitives
Selecting the right synchronization primitive in Rust minimizes contention and matches the exact sharing, mutation, and notification pattern of your concurrent workload.
Decision Tree for Multi-Threaded Synchronization
What is the multi-threaded access pattern?
|
+-- 1. Initialized once, then read-only forever?
| +-- Fixed initializer (`static` global) -----------> `LazyLock<T>`
| +-- Initialized at runtime with args --------------> `OnceLock<T>`
| +-- Heap value created first, then shared ---------> `Arc<T>` (no inner lock!)
|
+-- 2. Single counter, boolean flag, or bitmask?
| +--------------------------------------------------> `AtomicU64` / `AtomicUsize` / `AtomicBool`
|
+-- 3. Mutating a struct or collection (`Vec`, `HashMap`)?
| +-- General read/write or short critical section --> `Mutex<T>` (or `Sharded<Mutex<T>>`)
| +-- Expensive/frequent reads, rare writes ---------> `RwLock<T>`
|
+-- 4. Thread coordination & signaling?
+-- Producer-consumer ownership transfer ----------> `mpsc::channel` / `sync_channel`
+-- Sleep until custom predicate on `Mutex<T>` ----> `Mutex<T>` + `Condvar`
+-- Synchronize N threads at phase boundaries -----> `Barrier`
Complete Comparison of std::sync Primitives
| Primitive | Protects Data T? |
Read Concurrency | Write Concurrency | Blocks OS Thread? | Best Use Case |
|---|---|---|---|---|---|
Arc<T> |
Yes (immutable T) |
Unlimited (&T directly) |
Copy-on-write via Arc::make_mut |
Never | Sharing read-only configs, buffers, and services |
OnceLock<T> / LazyLock<T> |
Yes (write-once T) |
Unlimited (&T directly) |
1 thread once |
Only during first init | Global statics and lazy memoized caches |
AtomicBool / AtomicU64 |
Single primitive word | Unlimited (lock-free) | Lock-free (fetch_add, CAS) |
Never | Metrics, sequence numbers, cancellation flags |
Mutex<T> |
Yes (arbitrary T) |
1 thread at a time |
1 thread at a time |
Yes (futex on contention) | General shared mutable state with short critical sections |
RwLock<T> |
Yes (T: Send + Sync) |
$N$ readers simultaneously | 1 writer exclusively |
Yes (futex on contention) | Read-dominated routing tables and in-memory indexes |
Condvar |
Paired with Mutex<T> |
N/A | N/A | Yes (until .notify_*()) |
Bounded queues, resource pools, state-change waiters |
Barrier |
No (rendezvous only) | N/A | N/A | Yes (until $N$ threads arrive) | Iterative parallel simulations and phased benchmarks |
Combining OnceLock, AtomicU64, and Mutex in a Service Controller
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Mutex, OnceLock};
use std::thread;
struct ServiceController {
// 1. Write-once startup banner -> `OnceLock` (lock-free `&str` reads):
service_name: OnceLock<String>,
// 2. High-frequency counter -> `AtomicU64` (wait-free hardware increments):
requests_handled: AtomicU64,
// 3. Mutable error log -> `Mutex<Vec<String>>`:
recent_errors: Mutex<Vec<String>>,
}
impl ServiceController {
const fn new() -> Self {
Self {
service_name: OnceLock::new(),
requests_handled: AtomicU64::new(0),
recent_errors: Mutex::new(Vec::new()),
}
}
fn init(&self, name: &str) {
let _ = self.service_name.set(name.to_string());
}
fn handle_request(&self, succeed: bool) {
self.requests_handled.fetch_add(1, Ordering::Relaxed);
if !succeed {
self.recent_errors
.lock()
.unwrap()
.push(String::from("500 Internal Error"));
}
}
}
static CONTROLLER: ServiceController = ServiceController::new();
fn main() {
CONTROLLER.init("payment-gateway");
thread::scope(|s| {
s.spawn(|| CONTROLLER.handle_request(true));
s.spawn(|| CONTROLLER.handle_request(false));
s.spawn(|| CONTROLLER.handle_request(true));
});
println!(
"service = {:?}, requests = {}, errors = {:?}",
CONTROLLER.service_name.get().unwrap(),
CONTROLLER.requests_handled.load(Ordering::Relaxed),
*CONTROLLER.recent_errors.lock().unwrap()
);
}
service = "payment-gateway", requests = 3, errors = ["500 Internal Error"]