Rust / the practical course

03 / Concurrency

Atomics and memory ordering

Use atomic counters and understand why sharing other data needs more than a flag.

60 min + practiceRust 1.98.1 · Edition 2024

By Robert DeVore · Download Markdown

Increment a shared counter

Several threads increment one atomic counter. Each read-modify-write is atomic, so updates to that counter are not lost. We do not use the counter to announce that unrelated memory is ready.

Run the example

cargo run --locked --example 18_atomics
use std::sync::atomic::{AtomicUsize, Ordering};
fn main() {
    let count = AtomicUsize::new(0);
    std::thread::scope(|s| {
        for _ in 0..4 {
            s.spawn(|| {
                for _ in 0..100 {
                    count.fetch_add(1, Ordering::Relaxed);
                }
            });
        }
    });
    assert_eq!(count.load(Ordering::Relaxed), 400);
}

View the tested source

Relaxed is enough for the count's own atomic updates in this example. The scoped threads finish before the final assertion, giving the necessary completion synchronization. If the main thread read while workers were still running, it could observe an intermediate count. The counter does not mean “all workers are finished” by itself.

Publication is a different problem

Imagine one thread writes a payload and then sets a ready flag. Another waits for ready and reads the payload. A relaxed flag does not, by itself, establish the ordering needed to publish unrelated non-atomic memory safely. A release operation paired with an acquire operation that observes it, or an appropriate release sequence, can establish a happens-before relationship. You must also show that every access to the payload follows the memory model's rules.

SeqCst adds a global order for sequentially consistent atomic operations, subject to the model's rules. It does not turn a multi-step algorithm into a transaction, prevent deadlocks, or make ordinary conflicting memory accesses acceptable. Stronger ordering can simplify a proof, but it does not replace one.

We will not build a lock-free queue here. Besides memory ordering, it needs rules for ownership and memory reclamation, protection against values changing and changing back unnoticed (the ABA problem), and an argument that operations make progress under every allowed schedule.

Keep layers separate

The Rust atomic API specifies language-level behavior and follows a memory-model framework documented by the standard library. Supported atomic widths depend on the target. OS scheduling and hardware instructions also affect speed and progress. Rust's safety guarantees do not settle those questions.

An algorithm can be data-race-free and still produce an incorrect result. Two individually atomic operations may interleave with another thread between them. An atomic operation does not make the surrounding sequence of steps atomic. Even a sequence of atomics may need a mutex to implement the intended invariant simply.

Exercise

Change the number of workers and increments, compute the expected total, and assert it after joining. Then explain why replacing a future payload-ready flag with Relaxed would require a different argument from this counter.

Solution and acceptance check

With six workers and 250 increments, the final count is 1500. Choose a size that cannot wrap the target's usize. The result follows from atomic updates and completion synchronization. Publishing a payload asks whether other memory writes become visible and whether conflicting access is excluded; the count-only proof does not answer that question.

For a deeper treatment, read the standard atomic documentation and Mara Bos's memory-ordering chapter. These support the distinction between atomicity and synchronization; they do not constitute an endorsement of this course.

Find a lesson