Atomic Operations
Atomic operations (core::sync::atomic, re-exported in std::sync::atomic) perform indivisible hardware read, write, and read-modify-write operations on shared memory across threads and interrupt handlers without requiring OS mutex locks. Unlike normal reads and writes (which cause Undefined Behavior on concurrent access), atomic operations have a well-defined concurrent memory model and compile directly to single CPU instructions (lock xadd, ldadd, cas).
Non-Atomic `count += 1` (3 Separate Instructions -> Data Race / Lost Updates!):
Thread A: Load (0) -------------------> Add 1 ---> Store (1)
Thread B: Load (0) -> Add 1 ------------> Store (1) (Lost update!)
Atomic `count.fetch_add(1, Ordering::Relaxed)` (Hardware Indivisible RMW):
Thread A: [ Atomic Fetch-Add: Reads 0, Writes 1 ]
Thread B: [ Atomic Fetch-Add: Reads 1, Writes 2 ]
Standard Atomic Types in core::sync::atomic
All atomic types have the exact same size and bit representation as their underlying primitive type (size_of::<AtomicU64>() == size_of::<u64>()), and expose interior mutability (&self methods):
| Atomic Type | Underlying Primitive | Supported Operations |
|---|---|---|
AtomicBool |
bool (1 byte) |
load, store, swap, compare_exchange, fetch_and, fetch_or, fetch_xor, fetch_nand |
AtomicI8..AtomicI64, AtomicIsize |
i8..i64, isize |
All of the above plus fetch_add, fetch_sub, fetch_max, fetch_min, fetch_update |
AtomicU8..AtomicU64, AtomicUsize |
u8..u64, usize |
All of the above plus fetch_add, fetch_sub, fetch_max, fetch_min, fetch_update |
AtomicPtr<T> |
*mut T (pointer) |
load, store, swap, compare_exchange, fetch_ptr_add, fetch_byte_add, fetch_or (tag bits) |
Lock-Free State Transitions with compare_exchange_weak (CAS Loop)
When a state transition cannot be expressed as a simple addition or bitwise OR (for example, multiplying a number, or updating a maximum only when a custom condition holds), use a Compare-And-Swap (CAS) loop via compare_exchange_weak (or the helper method .fetch_update(...)):
compare_exchange(current, new, success_ord, failure_ord)atomically compares the value in memory withcurrent; if equal, it replaces it withnewand returnsOk(current), otherwise it returnsErr(actual_value_in_memory).- Inside a retry loop, prefer
compare_exchange_weak, which is allowed to fail spuriously on Load-Linked / Store-Conditional (LL/SC) architectures like ARM and RISC-V, generating tighter machine code when already wrapped in aloop:
use std::sync::atomic::{AtomicU64, Ordering};
use std::thread;
pub struct HighWaterMark {
peak: AtomicU64,
}
impl HighWaterMark {
pub const fn new(initial: u64) -> Self {
Self {
peak: AtomicU64::new(initial),
}
}
/// Atomically updates `peak` if `candidate` is strictly greater, returning the previous peak.
pub fn record(&self, candidate: u64) -> u64 {
let mut current = self.peak.load(Ordering::Relaxed);
while candidate > current {
match self.peak.compare_exchange_weak(
current,
candidate,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(prev) => return prev,
Err(actual) => current = actual, // Another thread updated `peak`; retry with `actual`
}
}
current
}
pub fn get(&self) -> u64 {
self.peak.load(Ordering::Relaxed)
}
}
fn main() {
let tracker = HighWaterMark::new(0);
thread::scope(|s| {
for sample in [45, 120, 88, 310, 205, 95] {
s.spawn(|| {
tracker.record(sample);
});
}
});
assert_eq!(tracker.get(), 310);
println!("Lock-free peak recorded across threads: {}", tracker.get());
}
Lock-free peak recorded across threads: 310
Checking Platform Atomic Availability (cfg(target_has_atomic))
While 64-bit desktop and server CPUs support all atomic widths from 8-bit to 64-bit, some 32-bit or 16-bit embedded microcontrollers (such as AVR or Thumbv6-M Cortex-M0) lack 64-bit or even CAS atomic instructions. In portable no_std libraries, use #[cfg(target_has_atomic = "64")] or #[cfg(target_has_atomic = "ptr")] to conditionally compile atomic code paths.