🎓 A1Learner
🔍
0 of 149 lessons completed

Rust Atomics

An atomic is a single value that many threads may change at once, safely. A Mutex can share a counter between threads, but for one plain number or one yes/no flag the atomic is the lighter tool.

An atomic wraps one plain value

let hits = AtomicUsize::new(0);
let flag = AtomicBool::new(false);

Read and write with load and store

Every access says its Ordering out loud — the next lesson is devoted to that word:

let now = hits.load(Ordering::SeqCst);
flag.store(true, Ordering::SeqCst);

fetch_add changes and reports, in one step

use std::sync::atomic::{AtomicUsize, Ordering};
fn main() {
    let hits = AtomicUsize::new(10);
    let before = hits.fetch_add(5, Ordering::SeqCst);
    println!("before = {}, now = {}", before, hits.load(Ordering::SeqCst));
}

swap trades the value in one step

use std::sync::atomic::{AtomicBool, Ordering};
fn main() {
    let flag = AtomicBool::new(true);
    let old = flag.swap(false, Ordering::SeqCst);
    println!("was = {}, now = {}", old, flag.load(Ordering::SeqCst));
}

Atomics can live in a static directly

Their new is a const fn, so a global atomic needs no lock and no lazy setup:

static REQUESTS: AtomicUsize = AtomicUsize::new(0);
// share it between threads as it stands — no Arc, no Mutex, no OnceLock

Example

use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;

fn main() {
    static HITS: AtomicUsize = AtomicUsize::new(0);
    thread::scope(|s| {
        for _ in 0..4 {
            s.spawn(|| {
                for _ in 0..250 {
                    HITS.fetch_add(1, Ordering::SeqCst);
                }
            });
        }
    });
    println!("total hits = {}", HITS.load(Ordering::SeqCst));
}

Key points

  • Atomics live in std::sync::atomic: AtomicBool, AtomicUsize, AtomicI32 and their relatives.
  • Each is one plain value.
  • Many threads may safely change it at once.
  • Every access names an Ordering (the next lesson).
  • SeqCst is the strongest ordering.
  • It is the safe default while you are learning.
  • Use fetch_add and fetch_sub for counters: the change, and the report of the old value, happen as one indivisible step
  • There is no guard and no lock() to wait for.
  • An atomic operation never blocks another thread the way a held Mutex does
  • An atomic protects only its own single value.
  • The moment two values must change together, a Mutex is the right tool again.
  • The same holds when the data is a whole struct.
  • An Arc's owner count is an atomic number.
  • It is updated with exactly these operations.

In this example, HITS is a static AtomicUsize. It is one counter shared by the whole program. It is set up with nothing but its const new. Four scoped threads each call fetch_add 250 times. fetch_add is a single indivisible step. No two threads can start from the same value. No update is lost between them. The scope joins all four threads before the last line loads the total: 4 × 250 = exactly 1000, on every run. A plain shared number could not do this. Changing one value from four threads breaks the borrow rules. A Mutex could do it, but at the price of a thousand lock-takings.

Note

Atomics are the engine under Arc's hood.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

Four threads each fetch_add the same AtomicUsize 250 times, with no lock anywhere. Why is the final count exactly 1000 and never less?