We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Atomics, Send/Sync and the Memory Model step 9 of 12
static mut and the edition-2024 wall
Drive a process-global counter from four threads, and report what it read along the way.
pub fn global_counter(ops: Vec<String>) -> Vec<i64>
Each op is "inc", "dec" or "read"; anything else is ignored. The inc
and dec ops between two reads form a batch, applied concurrently by
four scoped threads. A "read" acts as a sequence point: apply the pending
batch, then push the counter’s value. At the end, apply the last batch and
push the final value.
["inc","inc","read","dec","read"] -> [2, 1, 1]
[] -> [0]
["read"] -> [0, 0]
Addition commutes, so a batch’s result does not depend on which thread applied which delta — that is what makes a genuinely concurrent update testable.
The counter must be a real static, not a local. That is the point: this is
the global-mutable-state problem, seen from the concurrency side.
The wall
A C programmer writes this without a second thought:
static mut COUNTER: i64 = 0;
and edition 2024 stops them:
error: creating a mutable reference to mutable static
|
| let c = unsafe { &mut COUNTER };
| ^^^^^^^^^^^^ mutable reference to mutable static
|
= note: for more information, see
<https://doc.rust-lang.org/edition-guide/rust-2024/static-mut-references.html>
= note: `#[deny(static_mut_refs)]` (part of `#[deny(rust_2024_compatibility)]`)
on by default
Two things about that message are worth reading carefully.
It is deny-by-default, not a warning. No flag turns it off in this build,
and no amount of unsafe helps — unsafe lets you do the operation, and
this operation is not permitted at all.
It is the reference that is banned, not the static. Reading or writing
COUNTER directly inside an unsafe block still compiles today. That
distinction is the whole content of the lint: a &mut carries an aliasing
promise — “no other reference to this exists” — and for a global reachable
from every thread and every function in the program, that promise is
essentially never true. Two &mut COUNTER in scope at once is instant
undefined behaviour, and nothing stops you creating them.
(Note the error has no E code. It is the uncoded static_mut_refs lint.
You may see E0796 cited in older material; rustc --explain E0796 now
answers “this error code is no longer emitted by the compiler”.)
Why this restatement of Sync is the compact one
A &'static mut T obtained from a global is trivially aliasable from two
threads — just call the function twice, from two threads, and both get one.
That is the entire Sync story in one sentence: a type is shareable between
threads only if &T cannot be used to break its invariants, and &mut
obtained from a global breaks the most basic invariant there is.
static mut was the one hole in that story, and edition 2024 closed it.
The migration ladder
In the order you should try them:
-
An atomic.
static COUNTER: AtomicI64 = AtomicI64::new(0);— works becauseAtomicI64::newis aconst fn, needs nounsafe, and isSyncso every thread can reach it through&. This is the answer here. (1.95 addedupdate/try_updatefor read-modify-write closures.) -
Mutex/RwLock. Both areconst-constructible now, sostatic LOG: Mutex<Vec<String>> = Mutex::new(Vec::new());just works. Use this when the state is more than a machine word. -
OnceLock/LazyLockfor state that is initialised once and then only read. -
&raw mut STATIC— the sanctioned escape when you must hand a pointer to C. It creates a raw pointer without going through a reference, so no aliasing promise is made. This is a supported feature, not a loophole. -
A hand-rolled
SyncUnsafeCellwhen nothing above fits:struct SyncCell<T>(UnsafeCell<T>); unsafe impl<T: Send> Sync for SyncCell<T> {}and then you provide the synchronisation yourself.
std::cell::SyncUnsafeCellexists but is still unstable on 1.95, which is why people keep writing this by hand.
The static outlives your function
A static lives for the whole process, so a second call to global_counter
sees whatever the first one left behind. Reset it at the top, so the function
is idempotent and its result depends only on its argument.
This is not a harness quirk, it is what globals are. Any function whose answer depends on process history is a function you cannot test, and “make it idempotent or make it take its state as a parameter” is the fix in every language.
::: question const COUNTER: AtomicI64 = AtomicI64::new(0); compiles. What does a program using it print, and how many diagnostics does it get?
It prints 0 0 0 where the static version prints 0 1 2, and it collects
three diagnostics from two tools.
A const is a value inlined at every use site, so every mention of
COUNTER constructs a fresh atomic, increments it, and discards it. The
counter never changes and the program silently counts nothing.
clippy::declare_interior_mutable_const
clippy::borrow_interior_mutable_const
const_item_interior_mutations (rustc's own)
All three are warn-by-default, so all three fail this course’s gate. Three
independent lints firing on one line is the toolchain teaching the const
versus static distinction directly — and it is a good general signal: when
several unrelated analyses agree, the code is not merely unidiomatic.
:::
One free gate worth knowing
rustc’s invalid_atomic_ordering is deny-by-default and catches the
syntactically impossible orderings — a load(Release), a store(Acquire), a
compare_exchange whose failure ordering is stronger than its success
ordering. It grades that class of mistake for free. It cannot tell you that
Relaxed was too weak for what you meant; only reasoning does that.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.