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!