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):
Relaxed: only this one variable is atomic; no promises about other data. Right for counters and statistics that you read afterjoin(joining already synchronizes).Release(on a store) withAcquire(on a load): for publishing. Write your data, thenstore(true, Release)a flag; a thread that sees the flag withload(Acquire)is guaranteed to see the data too.SeqCst: the strictest; all threads agree on one order of allSeqCstoperations. The safe choice when you're unsure, and the cost is small on most hardware.
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
- Load, then store:
let n = c.load(Relaxed); c.store(n + 1, Relaxed);is two operations, and another thread can sneak in between them: lost updates. Usefetch_add(orcompare_exchange) for read-modify-write. - Several related values in separate atomics (a balance and a transaction count). Each is atomic on its own, but they can't change together. Use a
Mutexaround a struct. - Agonizing over
Orderingfor a counter.Relaxedis correct there. moveon a scoped thread that uses the counter:movewould move the counter itself into the first thread. If you needmove(for a loop variable), make a reference first,let hits = &hits;, and move that.
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