Synchronization Primitives

Atomic Types

Atomic types (defined in std::sync::atomic) provide primitive shared-memory variables (bool, integers, and raw pointers) whose loads, stores, swaps, and read-modify-write updates execute as indivisible hardware instructions without acquiring OS mutexes. Every atomic type has the exact same size and in-memory bit representation as its underlying primitive type, and implements Sync so it can be mutated through a shared reference &self across threads.

Complete Catalog of Standard Library Atomic Types

Atomic Type Underlying Primitive Size (64-bit) Key Type-Specific Operations
AtomicBool bool 1 byte load, store, swap, fetch_and, fetch_or, fetch_xor, fetch_nand
AtomicU8 / AtomicI8 u8 / i8 1 byte Arithmetic (fetch_add, fetch_sub, fetch_max, fetch_min) & bitwise ops
AtomicU16 / AtomicI16 u16 / i16 2 bytes Arithmetic & bitwise ops
AtomicU32 / AtomicI32 u32 / i32 4 bytes Arithmetic & bitwise ops
AtomicU64 / AtomicI64 u64 / i64 8 bytes Arithmetic & bitwise ops (#[cfg(target_has_atomic = "64")])
AtomicUsize / AtomicIsize usize / isize 8 bytes Arithmetic & bitwise ops
AtomicPtr<T> *mut T 8 bytes load, store, swap, compare_exchange, fetch_ptr_add, fetch_byte_add

Notice that there is no AtomicF32 or AtomicF64 in the standard library because CPUs do not provide native hardware fetch_add instructions on IEEE 754 floats (floating-point atomic updates are implemented using AtomicU32/AtomicU64 with f64::to_bits/from_bits and .fetch_update(...)).

Single-Instruction Atomic Read-Modify-Write (fetch_add vs. load + store)

A critical rule of atomic programming is that two separate atomic operations (x.store(x.load() + 1)) are NOT atomic as a combined unit! Between x.load() and x.store(), another thread can modify x, losing an increment.

Hardware read-modify-write methods—.fetch_add(), .fetch_sub(), .fetch_or(), .fetch_max(), and .swap()—perform the read, modification, and write in a single indivisible CPU instruction and return the previous value that was held before the operation:

Broken Two-Step Update (`x.store(x.load() + 1)`):
  Thread 1: loads 10 -----------------------> stores 11
  Thread 2:           loads 10 -> stores 11              (Increment lost! Final = 11)

Indivisible Read-Modify-Write (`x.fetch_add(1, Ordering::Relaxed)`):
  Thread 1: [ fetch_add(1): reads 10, writes 11 ]
  Thread 2:                                       [ fetch_add(1): reads 11, writes 12 ]
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::thread;

static SHUTDOWN_REQUESTED: AtomicBool = AtomicBool::new(false);
static TOTAL_BYTES: AtomicU64 = AtomicU64::new(0);
static PEAK_PACKET_SIZE: AtomicU64 = AtomicU64::new(0);

fn record_packet(size: u64) {
    // Indivisible atomic addition (returns previous total):
    TOTAL_BYTES.fetch_add(size, Ordering::Relaxed);
    // Indivisible atomic maximum update (updates only if `size` > current max):
    PEAK_PACKET_SIZE.fetch_max(size, Ordering::Relaxed);
}

fn main() {
    thread::scope(|s| {
        for packet_sizes in [[128_u64, 512], [1024, 64], [256, 2048]] {
            s.spawn(move || {
                for size in packet_sizes {
                    if !SHUTDOWN_REQUESTED.load(Ordering::Relaxed) {
                        record_packet(size);
                    }
                }
            });
        }
    });

    println!("total bytes:      {}", TOTAL_BYTES.load(Ordering::Relaxed));
    println!("peak packet size: {}", PEAK_PACKET_SIZE.load(Ordering::Relaxed));
}
total bytes:      4032
peak packet size: 2048

Viewing Mutable Primitives as Atomics (AtomicU64::from_mut)

When you have an exclusive reference &mut u64 (or &mut [u64]) and want to temporarily share it across scoped threads for parallel atomic updates without changing the struct field's type from u64 to AtomicU64, AtomicU64::from_mut(&mut val) (and AtomicU64::from_mut_slice(&mut slice)) converts &mut u64 into &AtomicU64 at zero cost!