Low-Level Systems Programming

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 with current; if equal, it replaces it with new and returns Ok(current), otherwise it returns Err(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 a loop:
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.