Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

6.3 · Atomic Types and Their Operations

Domain 6 — Concurrency and Parallelism Duration: ~15 minutes Library components: std::sync::atomic::AtomicBool, std::sync::atomic::AtomicUsize, std::sync::atomic::AtomicPtr, std::sync::atomic::Ordering

Introduction

Atomics are the smallest concurrency primitive. An atomic is a single value whose operations complete indivisibly, as dedicated CPU instructions. It needs no lock, no syscall, and no blocking. Each higher-level tool in this domain (Mutex, channels, OnceLock) uses atomics at its lowest level.

This tutorial shows:

  • The atomic type family and its portability limits.
  • load, store, swap: flags and one-winner claims.
  • The fetch_* read-modify-write (RMW) family: fetch_add, fetch_sub, fetch_or, fetch_and, fetch_xor.
  • Closure-based RMW: update/try_update (stabilized in Rust 1.95), and the legacy fetch_update (deprecated in Rust 1.99).
  • from_mut / from_mut_slice / get_mut_slice (1.98): exclusive &mut integers as atomics.
  • When an atomic is better than a Mutex, and when it is not.

Memory orderings appear here only as a general rule: Relaxed for standalone counters, SeqCst when you are not sure. Tutorial 6.4 gives the full model.

The Atomic Type Family

std::sync::atomic provides AtomicBool, AtomicI8/AtomicU8 through AtomicI64/AtomicU64, AtomicIsize/AtomicUsize, and AtomicPtr<T>. Three properties define them:

  • Interior mutability: all operations take &self. An atomic in a static needs no unsafe, no lazy initialization, and no mut:

    // `AtomicU64::new` is a const fn, so it can initialize a static.
    static REQUESTS_SERVED: AtomicU64 = AtomicU64::new(0);
  • Sync by design: the purpose of an atomic is that threads share a &AtomicU64. The hardware serializes concurrent operations.

  • Portability limit: not every target has every width (some 32-bit platforms have no 64-bit atomics). AtomicUsize and AtomicBool are the safest defaults. Each type exists only where the platform supports lock-free operations on it.

Each operation takes an Ordering parameter. Until Tutorial 6.4, use this rule. A counter that needs only its own consistency can use Relaxed. Code that publishes other memory should use SeqCst, the default when you are not sure.

A unique &mut u32 proves that no other thread can observe the value. Thus, since 1.98, you can view those bytes as an atomic with AtomicU32::from_mut(&mut n). from_mut_slice does the same for a full array. get_mut_slice is the inverse: it gives an exclusive &mut [AtomicU32] as an ordinary &mut [u32] for single-threaded initialization. The exclusive borrow does the work of a lock.

load, store, swap: Flags and Claims

load and store are atomic reads and writes. The usual pattern is a shutdown flag:

let shutdown = AtomicBool::new(false);
// The coordinator sets the flag. In a real program, it does this while the worker runs.
// `06_09_atomic_counter.rs` sets the flag BEFORE the worker starts, for a deterministic result.
shutdown.store(true, Ordering::Relaxed);

let iterations = thread::scope(|s| {
    s.spawn(|| {
        let mut work_done = 0_u32;
        // The worker checks the flag before each unit of work.
        while !shutdown.load(Ordering::Relaxed) {
            work_done += 1;
        }
        work_done
    })
    .join()
    .expect("worker should not panic")
});
assert_eq!(iterations, 0);   // the flag was already true at the first check

swap writes a new value and returns the old value in one indivisible step. This gives two common patterns:

  • One-winner claim: N threads race, and exactly one wins:

    // claimed: AtomicBool, initially false. 8 threads run this code.
    let already_claimed = claimed.swap(true, Ordering::Relaxed);   // returns the OLD value
    if !already_claimed {
        // Only one thread gets `false`. That thread runs the job.
    }
  • Take-and-reset: drain a dirty-flags word atomically. A flag that a different thread raises concurrently goes into this batch or the subsequent batch. The program never loses a flag and never counts a flag twice:

    let dirty = AtomicU8::new(0b0110);
    let batch = dirty.swap(0, Ordering::Relaxed);   // read AND clear in one step
    // batch is 0b0110, and dirty is now 0

Which thread wins a claim is different on each run. That exactly one thread wins is deterministic. These tutorials assert on this type of property.

fetch_add: The Canonical Lock-Free Counter

A non-atomic count += 1 from two threads is a lost-update bug: each thread reads 5, and each thread writes 6. fetch_add does the read-modify-write as one step:

// REQUESTS_SERVED is the static AtomicU64 from the first snippet.
// 4 threads each run this line 10_000 times:
REQUESTS_SERVED.fetch_add(1, Ordering::Relaxed);

// After the join of all the threads, the value is exactly 40_000 on each run:
assert_eq!(REQUESTS_SERVED.load(Ordering::Relaxed), 40_000);

Each fetch_* operation returns the previous value. Frequently, that value is the result that you need. For example, a lock-free ID allocator is only next_id.fetch_add(1, ...), because each caller gets a different previous value.

The bitwise family operates on flag words:

  • fetch_or sets bits. Each worker sets its own "done" bit. OR is commutative, and that is why no lock is necessary.
  • fetch_and clears bits.
  • fetch_xor toggles bits.

fetch_max and fetch_min also exist, for a running maximum or minimum.

06_09_atomic_counter.rs prints:

requests served: 40000
allocated ids: [100, 101, 102, 103]
worker exited after 0 iterations (flag was pre-set)

All assertions passed.

fetch_update, update, try_update: Arbitrary RMW

There is no fetch_clamp or fetch_saturating_mul. For an operation that is not built in, you supply a closure. Rust 1.95 stabilized the two current methods:

  • update(set_order, fetch_order, f) applies f: T -> T. It repeats an internal compare-exchange loop until its write succeeds. Then it returns the previous value:

    // peak_latency_us: AtomicU32, initially 0. Several threads share it.
    // sample: u32, one latency measurement.
    // Keep the maximum of all the samples from all the threads:
    peak_latency_us.update(Ordering::Relaxed, Ordering::Relaxed, |peak| peak.max(sample));
  • try_update(set_order, fetch_order, f) takes f: T -> Option<T>. If the closure returns None, the call stops and does not write. The result is Ok(previous) after a write, and Err(current) after a stop. A token bucket uses it to "decrement unless zero" in one atomic step:

    // tokens: AtomicU32, initially 3. 8 threads run this code.
    let outcome = tokens.try_update(Ordering::Relaxed, Ordering::Relaxed, |t| {
        t.checked_sub(1)   // None when t == 0: stop, no write
    });
    match outcome {
        Ok(_previous) => { /* this thread got a token: the previous value was 1 or more */ }
        Err(_current) => { /* the pool is empty: the current value is 0 */ }
    }

    With 3 tokens and 8 threads, which threads get a token is different on each run. That exactly 3 succeed and 5 fail is deterministic.

  • fetch_update is the original form with an Option, and its behavior is identical to try_update. It appears in much existing code. Rust 1.99 deprecates it ("renamed to try_update"), so the compiler gives a warning for each use. Write try_update in new code.

One rule applies: the closure may run more than one time, because the method tries again when a different thread wins the race. Keep the closure pure: no side effects, no I/O.

When an Atomic Is Better Than a Mutex, and When It Is Not

Figure: Choosing Between Atomics and Locks

Atomics are better for single independent values. They have no blocking, no syscalls, and no poisoning, and the hot path is one CPU instruction.

Atomics are worse when an invariant includes more than one value. Each of two separate atomics can be consistent while a reader sees the pair in an inconsistent state. Atomicity does not compose across variables. Mutex<Stats> makes the full struct one critical section. Two AtomicU64 values make two independent guarantees. When you are not sure, start with Mutex. Use an atomic only when a profiler shows the need.

Summary

ConceptKey point
Atomic typesAtomicBool, integer widths, AtomicPtr. Operations take &self. new is a const fn.
Staticsstatic N: AtomicU64 = AtomicU64::new(0) needs no unsafe and no lazy initialization.
load / storeAtomic read and write, for shutdown flags and published state.
swapWrites a value and returns the old value: one-winner claims, take-and-reset.
fetch_add etc.Indivisible RMW. Returns the previous value (ID allocator).
fetch_or/and/xorLock-free bit operations. Commutative operations make a lock unnecessary.
update (1.95)Closure T -> T, repeated compare-exchange loop, returns the previous value.
try_update (1.95)Closure T -> Option<T>. None stops the call without a write.
fetch_updateLegacy name of try_update, deprecated since 1.99.
from_mut / from_mut_slice / get_mut_slice (1.98)Exclusive &mut integers ↔ atomics. The exclusive borrow does the work of a lock.
Closure ruleThe closure may run more than one time. Keep it pure.
Atomic vs MutexAtomic for one value. Mutex for invariants across values.

Code Examples

FileDescription
06_09_atomic_counter.rsfetch_add counters, static atomics, ID allocation, shutdown flag
06_10_atomic_bitflags.rsfetch_or/and/xor, swap take-and-reset, one-winner claim
06_11_atomic_update.rsupdate/try_update (1.95), token bucket, fetch_update (deprecated in 1.99)
06_28_atomic_from_mut.rsfrom_mut / from_mut_slice / get_mut_slice (1.98)