RustArena

Step 6 of 7

Atomics: lock-free counters and flags

For a single number or flag, a Mutex is heavier than it needs to be. Atomic types from std::sync::atomic (AtomicUsize, AtomicU64, AtomicI32, AtomicBool, ...) are plain integers and booleans whose operations are single, indivisible CPU instructions: no thread ever sees a half-done update, and there's no lock to wait for. Their methods take &self, so a shared reference (or an Arc) is all a thread needs.

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

fn main() {
    let requests = vec!["GET /", "POST /login", "GET /shop", "GET /", "DELETE /cart", "GET /about"];
    let gets = AtomicUsize::new(0);
    thread::scope(|s| {
        for chunk in requests.chunks(2) {
            s.spawn(|| {
                for line in chunk.iter() {
                    if line.starts_with("GET") {
                        gets.fetch_add(1, Ordering::Relaxed);
                    }
                }
            });
        }
    });
    println!("GET requests: {}", gets.load(Ordering::Relaxed));
}
GET requests: 4

Every thread borrows the one gets counter; fetch_add adds 1 in a single step that no other thread can interrupt. Notice chunk.iter() rather than for line in chunk: a closure that uses the loop variable chunk directly borrows that variable, which disappears at the end of each turn of the loop (E0373). Calling a method on it borrows only the data it points to, which lives as long as requests.

The operations you'll use most:

Method What it does
load(ord) / store(v, ord) read / write the value
fetch_add(n, ord), fetch_sub(n, ord) add / subtract in one step, returning the previous value
fetch_max(v, ord), fetch_min(v, ord) keep the larger / smaller value
swap(v, ord) write a new value and return the old one
compare_exchange(expected, new, ...) write new only if the value is still expected

Ordering, briefly

Every operation takes an Ordering. It tells the compiler and the CPU how much they may reorder other memory accesses around this one (both reorder freely to go faster):

Memory ordering is a deep topic; Mara Bos's free book Rust Atomics and Locks covers it properly. For everyday code: counters use Relaxed, flags that hand over data use Release/Acquire.

A stop flag

AtomicBool is the standard way to tell a background thread to stop:

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;

fn main() {
    let stop = Arc::new(AtomicBool::new(false));
    let flag = Arc::clone(&stop);
    let worker = thread::spawn(move || {
        while !flag.load(Ordering::Acquire) {
            thread::sleep(Duration::from_millis(1));
        }
        "worker stopped cleanly"
    });
    thread::sleep(Duration::from_millis(5));
    stop.store(true, Ordering::Release);
    println!("{}", worker.join().unwrap());
}
worker stopped cleanly

Common mistakes

Your task

Write fn count_matching(lines: &[&str], needle: &str, workers: usize) -> usize. Split lines into chunks of lines.len().div_ceil(workers).max(1), scan each chunk in its own scoped thread, and count the lines containing needle with one shared AtomicUsize (fetch_add). Return the final count.

Challenge 2

Fill in the atomic operation that adds 1 and can't lose updates. The program should print dropped packets: 300.

Challenge 3

Three arena servers report their match scores. In one thread::scope, give each server's list its own thread. Track the highest score with a shared AtomicU32 and fetch_max, and the number of games with a shared AtomicUsize. Print:

best score: 97
games: 9

Previous: RwLock, and how deadlocks happen Next: Global state: OnceLock and LazyLock