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
SeqCstwhen unsure (safest default) Relaxedis fastest but weakest orderingcompare_exchangefor lock-free updatescompare_exchange_weakin loops for performance- Atomics work on primitive types only
- For complex data, use
MutexorRwLock - Avoid spin-waits in production code
- Atomics are the building blocks for lock-free structures
