How do I work with Atomic Types for Lock-Free Programming in Rust?

Walkthrough

Atomic types provide lock-free concurrent access to primitive values. They enable safe mutation across threads without using mutexes, using CPU-level atomic instructions. Rust's std::sync::atomic module provides atomic versions of primitive types.

Key concepts:

  • AtomicBool, AtomicI32, AtomicUsize, etc. — Atomic primitive types
  • Ordering — Memory ordering constraints (Relaxed, Acquire, Release, SeqCst)
  • load() / store() — Basic read and write operations
  • compare_exchange() — Conditional atomic update (CAS)
  • fetch_add() / fetch_sub() — Atomic arithmetic with fetch

When to use Atomic Types:

  • Simple counters and flags shared across threads
  • Lock-free data structures
  • Performance-critical synchronization
  • Implementing higher-level primitives

When NOT to use Atomic Types:

  • Complex shared data (use Mutex)
  • When you need to guard multiple values atomically
  • Simple single-threaded code

Code Examples

Basic Atomic Counter

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
 
fn main() {
    let counter = Arc::new(AtomicUsize::new(0));
    let mut handles = vec![];
    
    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            for _ in 0..1000 {
                counter.fetch_add(1, Ordering::SeqCst);
            }
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    println!("Final count: {}", counter.load(Ordering::SeqCst));
}

AtomicBool for Flag

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
 
fn main() {
    let flag = Arc::new(AtomicBool::new(false));
    
    let flag_clone = Arc::clone(&flag);
    let setter = thread::spawn(move || {
        thread::sleep(std::time::Duration::from_millis(100));
        flag_clone.store(true, Ordering::SeqCst);
        println!("Flag set to true");
    });
    
    // Spin-wait (not recommended for production, use Condvar instead)
    while !flag.load(Ordering::SeqCst) {
        thread::yield_now();
    }
    
    println!("Flag was set!");
    setter.join().unwrap();
}

Memory Ordering Explained

use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
 
fn main() {
    let ready = AtomicBool::new(false);
    let data = AtomicI32::new(0);
    
    // Thread 1: Write data, then set ready
    data.store(42, Ordering::Release);  // Release ensures prior writes are visible
    ready.store(true, Ordering::Release);
    
    // Thread 2: Check ready, then read data
    if ready.load(Ordering::Acquire) {  // Acquire ensures we see prior writes
        println!("Data: {}", data.load(Ordering::Acquire));
    }
    
    // Ordering levels (from weakest to strongest):
    // Relaxed: No ordering guarantees, only atomicity
    // Release: Prior writes visible to threads that do Acquire
    // Acquire: See writes from prior Release operations
    // AcqRel: Both Acquire and Release
    // SeqCst: Total ordering across all SeqCst operations
}

Compare and Exchange (CAS)

use std::sync::atomic::{AtomicI32, Ordering};
 
fn main() {
    let value = AtomicI32::new(5);
    
    // Try to swap 5 -> 10
    let result = value.compare_exchange(
        5,              // Expected current value
        10,             // New value if current matches expected
        Ordering::SeqCst,  // Success ordering
        Ordering::SeqCst,  // Failure ordering
    );
    
    match result {
        Ok(old) => println!("Swapped {} -> 10", old),
        Err(actual) => println!("Failed, actual value is {}", actual),
    }
    
    // Try again (will fail)
    let result = value.compare_exchange(5, 20, Ordering::SeqCst, Ordering::SeqCst);
    match result {
        Ok(old) => println!("Swapped {} -> 20", old),
        Err(actual) => println!("Failed, actual value is {}", actual),
    }
}

Atomic Pointer

use std::sync::atomic::{AtomicPtr, Ordering};
use std::ptr;
 
struct Data {
    value: i32,
}
 
fn main() {
    let ptr = AtomicPtr::new(ptr::null_mut());
    
    // Store a value
    let data = Box::into_raw(Box::new(Data { value: 42 }));
    ptr.store(data, Ordering::SeqCst);
    
    // Load and use
    let loaded = ptr.load(Ordering::SeqCst);
    unsafe {
        if !loaded.is_null() {
            println!("Value: {}", (*loaded).value);
        }
    }
    
    // Clean up
    unsafe {
        drop(Box::from_raw(loaded));
    }
}

Spin Lock Implementation

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
 
struct SpinLock {
    locked: AtomicBool,
}
 
impl SpinLock {
    fn new() -> Self {
        Self {
            locked: AtomicBool::new(false),
        }
    }
    
    fn lock(&self) {
        while self.locked.compare_exchange(
            false,
            true,
            Ordering::Acquire,
            Ordering::Relaxed,
        ).is_err() {
            // Spin until we acquire the lock
            while self.locked.load(Ordering::Relaxed) {
                std::hint::spin_loop();
            }
        }
    }
    
    fn unlock(&self) {
        self.locked.store(false, Ordering::Release);
    }
}
 
fn main() {
    let lock = Arc::new(SpinLock::new());
    let mut handles = vec![];
    
    for i in 0..3 {
        let lock = Arc::clone(&lock);
        handles.push(thread::spawn(move || {
            lock.lock();
            println!("Thread {} has lock", i);
            thread::sleep(std::time::Duration::from_millis(50));
            lock.unlock();
            println!("Thread {} released lock", i);
        }));
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

Atomic Counter with fetch_update

use std::sync::atomic::{AtomicU32, Ordering};
 
fn main() {
    let counter = AtomicU32::new(1);
    
    // Only allow values up to 100
    let result = counter.fetch_update(
        Ordering::SeqCst,
        Ordering::SeqCst,
        |x| if x < 100 { Some(x + 1) } else { None },
    );
    
    match result {
        Ok(old) => println!("Updated from {} to {}", old, old + 1),
        Err(current) => println!("Update rejected, current: {}", current),
    }
}

Atomic for ID Generation

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
 
struct IdGenerator {
    next_id: AtomicU64,
}
 
impl IdGenerator {
    fn new(start: u64) -> Self {
        Self {
            next_id: AtomicU64::new(start),
        }
    }
    
    fn next(&self) -> u64 {
        self.next_id.fetch_add(1, Ordering::Relaxed)
    }
    
    fn next_batch(&self, count: u64) -> u64 {
        self.next_id.fetch_add(count, Ordering::Relaxed)
    }
}
 
fn main() {
    let gen = Arc::new(IdGenerator::new(1000));
    let mut handles = vec![];
    
    for i in 0..5 {
        let gen = Arc::clone(&gen);
        handles.push(thread::spawn(move || {
            let id = gen.next();
            println!("Thread {} got id: {}", i, id);
        }));
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

Atomic for Statistics

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
 
struct Stats {
    requests: AtomicUsize,
    errors: AtomicUsize,
    bytes_sent: AtomicUsize,
}
 
impl Stats {
    fn new() -> Self {
        Self {
            requests: AtomicUsize::new(0),
            errors: AtomicUsize::new(0),
            bytes_sent: AtomicUsize::new(0),
        }
    }
    
    fn record_request(&self, bytes: usize, error: bool) {
        self.requests.fetch_add(1, Ordering::Relaxed);
        self.bytes_sent.fetch_add(bytes, Ordering::Relaxed);
        if error {
            self.errors.fetch_add(1, Ordering::Relaxed);
        }
    }
    
    fn snapshot(&self) -> (usize, usize, usize) {
        (
            self.requests.load(Ordering::Relaxed),
            self.errors.load(Ordering::Relaxed),
            self.bytes_sent.load(Ordering::Relaxed),
        )
    }
}
 
fn main() {
    let stats = Arc::new(Stats::new());
    let mut handles = vec![];
    
    for i in 0..5 {
        let stats = Arc::clone(&stats);
        handles.push(thread::spawn(move || {
            for j in 0..100 {
                stats.record_request(1024, j % 20 == 0);
            }
        }));
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
    
    let (reqs, errs, bytes) = stats.snapshot();
    println!("Requests: {}, Errors: {}, Bytes: {}", reqs, errs, bytes);
}

compare_exchange_weak for Loops

use std::sync::atomic::{AtomicI32, Ordering};
 
fn main() {
    let value = AtomicI32::new(0);
    
    // compare_exchange_weak may fail spuriously
    // Use in a loop for better performance
    let mut current = value.load(Ordering::Relaxed);
    loop {
        let new_value = current + 1;
        match value.compare_exchange_weak(
            current,
            new_value,
            Ordering::SeqCst,
            Ordering::Relaxed,
        ) {
            Ok(_) => break,
            Err(actual) => current = actual,
        }
    }
    
    println!("Value: {}", value.load(Ordering::Relaxed));
}

Atomic for Lazy Initialization

use std::sync::atomic::{AtomicPtr, Ordering};
use std::sync::Arc;
 
struct Config {
    name: String,
}
 
impl Config {
    fn new() -> Self {
        println!("Creating config...");
        Self {
            name: String::from("app-config"),
        }
    }
}
 
static CONFIG: AtomicPtr<Config> = AtomicPtr::new(std::ptr::null_mut());
 
fn get_config() -> &'static Config {
    let mut ptr = CONFIG.load(Ordering::Acquire);
    
    if ptr.is_null() {
        let new_config = Box::into_raw(Box::new(Config::new()));
        
        match CONFIG.compare_exchange(
            std::ptr::null_mut(),
            new_config,
            Ordering::AcqRel,
            Ordering::Acquire,
        ) {
            Ok(_) => ptr = new_config,
            Err(existing) => {
                // Another thread got there first
                unsafe { drop(Box::from_raw(new_config)); }
                ptr = existing;
            }
        }
    }
    
    unsafe { &*ptr }
}
 
fn main() {
    let config = get_config();
    println!("Config: {}", config.name);
}

fetch_max and fetch_min

use std::sync::atomic::{AtomicI32, Ordering};
 
fn main() {
    let value = AtomicI32::new(10);
    
    let old = value.fetch_max(5, Ordering::SeqCst);
    println!("Old: {}, New: {}", old, value.load(Ordering::SeqCst)); // 10, 10
    
    let old = value.fetch_max(20, Ordering::SeqCst);
    println!("Old: {}, New: {}", old, value.load(Ordering::SeqCst)); // 10, 20
    
    let old = value.fetch_min(15, Ordering::SeqCst);
    println!("Old: {}, New: {}", old, value.load(Ordering::SeqCst)); // 20, 15
}

Atomic Bit Flags

use std::sync::atomic::{AtomicUsize, Ordering};
 
const FLAG_A: usize = 1 << 0;
const FLAG_B: usize = 1 << 1;
const FLAG_C: usize = 1 << 2;
 
struct Flags {
    bits: AtomicUsize,
}
 
impl Flags {
    fn new() -> Self {
        Self { bits: AtomicUsize::new(0) }
    }
    
    fn set(&self, flag: usize) {
        self.bits.fetch_or(flag, Ordering::Relaxed);
    }
    
    fn clear(&self, flag: usize) {
        self.bits.fetch_and(!flag, Ordering::Relaxed);
    }
    
    fn toggle(&self, flag: usize) {
        self.bits.fetch_xor(flag, Ordering::Relaxed);
    }
    
    fn is_set(&self, flag: usize) -> bool {
        (self.bits.load(Ordering::Relaxed) & flag) != 0
    }
}
 
fn main() {
    let flags = Flags::new();
    
    flags.set(FLAG_A | FLAG_B);
    println!("A: {}, B: {}, C: {}", 
        flags.is_set(FLAG_A), 
        flags.is_set(FLAG_B), 
        flags.is_set(FLAG_C));
    
    flags.clear(FLAG_A);
    flags.toggle(FLAG_C);
    println!("A: {}, B: {}, C: {}", 
        flags.is_set(FLAG_A), 
        flags.is_set(FLAG_B), 
        flags.is_set(FLAG_C));
}

SeqLock Pattern

use std::sync::atomic::{AtomicUsize, Ordering};
 
struct SeqLock<T> {
    seq: AtomicUsize,
    data: T,
}
 
impl<T: Copy> SeqLock<T> {
    fn new(data: T) -> Self {
        Self {
            seq: AtomicUsize::new(0),
            data,
        }
    }
    
    fn read(&self) -> T {
        loop {
            let seq1 = self.seq.load(Ordering::Acquire);
            if seq1 & 1 != 0 {
                // Write in progress, retry
                std::hint::spin_loop();
                continue;
            }
            
            let data = self.data; // Copy
            
            let seq2 = self.seq.load(Ordering::Acquire);
            if seq1 == seq2 {
                return data;
            }
        }
    }
    
    fn write(&mut self, data: T) {
        let seq = self.seq.load(Ordering::Relaxed);
        self.seq.store(seq + 1, Ordering::Release); // Odd = writing
        
        self.data = data;
        
        self.seq.store(seq + 2, Ordering::Release); // Even = done
    }
}

Atomic for Rate Limiting

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Instant, Duration};
 
struct RateLimiter {
    tokens: AtomicU64,
    max_tokens: u64,
    refill_rate: u64, // tokens per second
    last_refill: AtomicU64, // stores timestamp as u64
}
 
impl RateLimiter {
    fn new(max_tokens: u64, refill_rate: u64) -> Self {
        Self {
            tokens: AtomicU64::new(max_tokens),
            max_tokens,
            refill_rate,
            last_refill: AtomicU64::new(0),
        }
    }
    
    fn try_acquire(&self) -> bool {
        // Simplified - in practice use proper time handling
        let tokens = self.tokens.load(Ordering::Relaxed);
        if tokens > 0 {
            self.tokens.fetch_sub(1, Ordering::Relaxed);
            true
        } else {
            false
        }
    }
}
 
fn main() {
    let limiter = Arc::new(RateLimiter::new(10, 5));
    
    for i in 0..15 {
        if limiter.try_acquire() {
            println!("Request {} allowed", i);
        } else {
            println!("Request {} denied", i);
        }
    }
}

Available Atomic Types

use std::sync::atomic::{
    AtomicBool, AtomicI8, AtomicI16, AtomicI32, AtomicI64, AtomicIsize,
    AtomicU8, AtomicU16, AtomicU32, AtomicU64, AtomicUsize,
    AtomicPtr,
};
 
fn main() {
    // Integer atomics
    let a_bool = AtomicBool::new(false);
    let a_i32 = AtomicI32::new(0);
    let a_u64 = AtomicU64::new(0);
    let a_usize = AtomicUsize::new(0);
    
    // Pointer atomics
    let a_ptr: AtomicPtr<i32> = AtomicPtr::new(std::ptr::null_mut());
    
    println!("Atomic types available for all integer sizes");
}

Summary

Atomic Types:

Type Description
AtomicBool Boolean atomic
AtomicI8/16/32/64/size Signed integer atomics
AtomicU8/16/32/64/size Unsigned integer atomics
AtomicPtr<T> Raw pointer atomic

Memory Orderings:

Ordering Description
Relaxed No ordering, only atomicity
Release Writes before become visible after Acquire
Acquire See writes from prior Release
AcqRel Both Acquire and Release
SeqCst Total global ordering (safest)

Common Operations:

Method Description
load(ord) Read value
store(val, ord) Write value
swap(val, ord) Exchange value, return old
compare_exchange(exp, new, ok, fail) CAS, returns Result
compare_exchange_weak(exp, new, ok, fail) Spurious CAS, use in loop
fetch_add/sub/and/or/xor/max/min(val, ord) Op with fetch

Key Points:

  • Use SeqCst when unsure (safest default)
  • Relaxed is fastest but weakest ordering
  • compare_exchange for lock-free updates
  • compare_exchange_weak in loops for performance
  • Atomics work on primitive types only
  • For complex data, use Mutex or RwLock
  • Avoid spin-waits in production code
  • Atomics are the building blocks for lock-free structures