We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Atomics, Send/Sync and the Memory Model step 1 of 12
Atomics: counters, flags and fetch_*
Build a histogram in parallel with no locks at all.
pub fn atomic_histogram(values: Vec<usize>, buckets: usize, workers: usize) -> Vec<usize>
Each value lands in bucket v % buckets. Split the values across workers
scoped threads that all increment the same Vec<AtomicUsize>, then return the
counts. buckets == 0 returns []; treat workers == 0 as 1.
The tests run workers = 1, 4, 16 and demand identical counts.
The cheapest correct shared state
An atomic is a value the hardware can read-modify-write in one indivisible
step. fetch_add compiles to a single instruction on every platform you care
about — no lock, no guard, no blocking, no poisoning, nothing to hold too
long, nothing to acquire in the wrong order.
let n = counter.fetch_add(1, Ordering::Relaxed);
Every fetch_* returns the PREVIOUS value, which is genuinely useful —
fetch_add(1, ..) hands you a unique ticket number as a side effect of
counting. It also catches people out, so read it once and remember it.
The portable set: AtomicBool, AtomicUsize/AtomicIsize, AtomicPtr, and
AtomicU8/16/32/64 with their signed twins. The 64-bit ones are missing on
some 32-bit targets, gated behind cfg(target_has_atomic = "64").
Why there is no Arc in this problem
Atomics are Sync. So &AtomicUsize is Send, so a shared reference to one
can cross a thread boundary — and with thread::scope supplying the lifetime,
&Vec<AtomicUsize> is all you need. No Arc, no clone per worker, no
atomic refcount to pay for on top of the atomic you actually wanted.
This is the payoff for having learned Sync as “T: Sync means &T: Send“
rather than as a magic word.
“Relaxed is enough here” — and why
You will meet the memory orderings properly in the next two items. For a
histogram the answer is Relaxed, and the reason is worth having now:
each counter stands entirely alone. No worker publishes data that another
worker reads. Nobody uses a count to decide whether some other memory is
ready. There is nothing to order against, so the only guarantee needed is the
one Relaxed gives: the operation is atomic, and the modifications to that
one variable have a single agreed order.
Relaxed does not mean “fast and slightly wrong”. It means “atomic, with
no promises about anything else”. When there is nothing else, it is exactly
right. Reach for SeqCst by default and you have written a slower program and
told the next reader that ordering matters here, which is worse than being
slow.
The obstacle in the starter
error[E0277]: the trait bound `AtomicUsize: Clone` is not satisfied
| let counts: Vec<AtomicUsize> = vec![AtomicUsize::new(0); buckets];
vec![elem; n] evaluates elem once and clones it. Atomics deliberately
are not Clone: cloning a counter would give you a second, independent
counter that shares nothing with the first, which is never what anybody
writing that line meant. The type system refuses rather than surprising you.
let counts: Vec<AtomicUsize> = (0..buckets).map(|_| AtomicUsize::new(0)).collect();
And at the end, into_iter().map(AtomicUsize::into_inner) reads them all out
without any synchronisation, because owning the value proves nobody else can
reach it.
::: question Why is const COUNTER: AtomicUsize = AtomicUsize::new(0); a bug, and how many warnings does it produce?
Three, from two different tools, and all of them warn-by-default — so all
three fail this course’s gate.
warning: a `const` item should not be interior mutable
[clippy::declare_interior_mutable_const]
warning: a `const` item with interior mutability should not be borrowed
[clippy::borrow_interior_mutable_const]
warning: taking a mutable reference to a `const` item
[const_item_interior_mutations] <- rustc's own
A const is not a variable, it is a value that is inlined at every use
site. So COUNTER.fetch_add(1, Relaxed) creates a brand-new atomic,
increments it, and throws it away. Every single time. The counter never
changes and the program silently counts nothing.
static COUNTER: AtomicUsize = AtomicUsize::new(0); is the fix: a static is
one object at one address that everything refers to. And it works with no
ceremony because AtomicUsize::new is a const fn — the same reason
static M: Mutex<Vec<u8>> = Mutex::new(Vec::new()) needs no lazy_static any
more.
Three warn-by-default lints firing on one line is unusually generous. The
toolchain is teaching the const versus static distinction directly, which
is worth taking as a general lesson: when several independent lints agree, the
code is not merely unidiomatic.
:::
One thing this build changes
The grader compiles with -O, which means release mode: integer overflow
wraps silently rather than panicking. A fetch_add on an AtomicU8 past
255 wraps to 0 and says nothing. It cannot happen with the inputs here, and it
is worth knowing before you size a counter: use usize/u64, or say
wrapping and mean it.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.