Introduction
Atomic types provide lock-free, thread-safe operations on primitive values. Unlike Mutex, which blocks other threads while one thread holds the lock, atomic operations complete in a single CPU instruction — no thread ever waits for a lock. This makes them the foundation of high-performance concurrent data structures, counters, flags, and synchronization primitives.
Key Concepts
- Atomic Types:
AtomicBool,AtomicI8/AtomicI16/AtomicI32/AtomicI64/AtomicIsize,AtomicU8/AtomicU16/AtomicU32/AtomicU64/AtomicUsize, andAtomicPtr<T>. Each wraps a primitive type and provides atomic operations. - load / store: Read or write the value atomically. Every access to an atomic must specify a memory ordering.
- fetch_add / fetch_sub / fetch_and / fetch_or / fetch_xor: Read-modify-write operations that execute atomically. They return the previous value before the operation was applied.
- compare_exchange: The compare-and-swap (CAS) operation. Atomically compares the current value to an expected value; if they match, it writes a new value and returns
Ok(previous). If they do not match, it returnsErr(current_value)without modifying anything. - Static Atomics: Atomic types implement
const fn new(), so they can be used directly instaticdeclarations withoutArcor lazy initialization.
Real World Context
Atomics are everywhere in systems programming. Reference counts in Arc use AtomicUsize internally. Lock-free queues, hazard pointers, and epoch-based reclamation all build on atomic operations. Simple use cases include global request counters, feature flags that can be toggled at runtime, and shutdown signals. When you need a shared counter or flag and a full Mutex feels like overkill, atomics are the right tool.
Deep Dive
The simplest atomic pattern is a shared counter. Unlike Arc<Mutex<usize>>, an AtomicUsize requires no lock acquisition — the CPU handles the atomicity directly.
rustuse std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::thread; let counter = Arc::new(AtomicUsize::new(0)); let handles: Vec<_> = (0..10) .map(|_| { let counter = Arc::clone(&counter); thread::spawn(move || { for _ in 0..1000 { counter.fetch_add(1, Ordering::Relaxed); } }) }) .collect(); for h in handles { h.join().unwrap(); } println!("count: {}", counter.load(Ordering::Relaxed)); // 10000
Atomic types can be declared as static variables, which avoids the need for Arc entirely. This is the most common pattern for global counters and flags.
rustuse std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; static RUNNING: AtomicBool = AtomicBool::new(true); static REQUEST_COUNT: AtomicU64 = AtomicU64::new(0); fn handle_request() { REQUEST_COUNT.fetch_add(1, Ordering::Relaxed); // ... handle the request ... } fn shutdown() { RUNNING.store(false, Ordering::Release); } fn is_running() -> bool { RUNNING.load(Ordering::Acquire) }
The store and load methods atomically write and read the value. They are the simplest atomic operations.
rustuse std::sync::atomic::{AtomicI32, Ordering}; let value = AtomicI32::new(10); value.store(42, Ordering::SeqCst); let current = value.load(Ordering::SeqCst); println!("current: {current}"); // 42
The fetch_* family of operations read the current value, apply an operation, and store the result — all atomically. Critically, they return the value before the operation.
rustuse std::sync::atomic::{AtomicI32, Ordering}; let value = AtomicI32::new(10); // fetch_add: returns old value, then adds let old = value.fetch_add(5, Ordering::SeqCst); assert_eq!(old, 10); // returned the OLD value assert_eq!(value.load(Ordering::SeqCst), 15); // now 15 // fetch_sub: returns old value, then subtracts let old = value.fetch_sub(3, Ordering::SeqCst); assert_eq!(old, 15); assert_eq!(value.load(Ordering::SeqCst), 12); // Bitwise operations work too let flags = AtomicI32::new(0b1100); let old = flags.fetch_or(0b0011, Ordering::SeqCst); assert_eq!(old, 0b1100); assert_eq!(flags.load(Ordering::SeqCst), 0b1111);
compare_exchange is the most powerful atomic operation — the foundation of lock-free algorithms. It compares the current value to an expected value and, only if they match, replaces it with a new value.
rustuse std::sync::atomic::{AtomicI32, Ordering}; let value = AtomicI32::new(5); // Success: current value (5) matches expected (5) match value.compare_exchange(5, 10, Ordering::SeqCst, Ordering::SeqCst) { Ok(prev) => println!("swapped: {prev} -> 10"), // prev is 5 Err(actual) => println!("failed, current is {actual}"), } // Failure: current value (10) does not match expected (5) match value.compare_exchange(5, 20, Ordering::SeqCst, Ordering::SeqCst) { Ok(prev) => println!("swapped: {prev} -> 20"), Err(actual) => println!("failed, current is {actual}"), // actual is 10 }
AtomicPtr<T> stores a raw pointer atomically. It is the building block for lock-free data structures like stacks and queues.
rustuse std::sync::atomic::{AtomicPtr, Ordering}; use std::ptr; let data = Box::new(42); let atomic_ptr = AtomicPtr::new(Box::into_raw(data)); // Atomically load the pointer let ptr = atomic_ptr.load(Ordering::Acquire); unsafe { println!("value: {}", *ptr); } // Clean up unsafe { drop(Box::from_raw(atomic_ptr.load(Ordering::Acquire))); }
Common Pitfalls
- Using the wrong memory ordering —
Ordering::Relaxedprovides no synchronization guarantees beyond atomicity. If you are using an atomic to communicate data between threads (not just count), you likely needAcquire/ReleaseorSeqCst. - Assuming
fetch_addreturns the new value — Allfetch_*operations return the previous value. If you need the value after the operation, compute it yourself:let new = counter.fetch_add(1, Ordering::Relaxed) + 1;.
Best Practices
- Use
staticatomics for global counters and flags — NoArcneeded. Atomic types have aconst fn new()constructor that works in static context. - Start with
SeqCstand optimize later —SeqCstis the safest ordering. Only weaken toAcquire/ReleaseorRelaxedafter you understand the ordering requirements and have verified correctness.
Summary
- Atomic types (
AtomicBool,AtomicI*,AtomicU*,AtomicPtr) provide lock-free thread-safe operations on primitive values. load/storeatomically read/write;fetch_add/fetch_sub/fetch_or/fetch_xorperform atomic read-modify-write and return the previous value.compare_exchangeis the CAS primitive underlying lock-free algorithms.- Static atomics avoid
Arcoverhead and are the standard pattern for global counters and flags.
Code Examples
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
static GLOBAL_COUNTER: AtomicU64 = AtomicU64::new(0);
fn main() {
let handles: Vec<_> = (0..8)
.map(|_| {
thread::spawn(|| {
for _ in 0..1000 {
GLOBAL_COUNTER.fetch_add(1, Ordering::Relaxed);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
println!("Total: {}", GLOBAL_COUNTER.load(Ordering::Relaxed));
// Always prints 8000
}