We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Smart Pointers and Interior Mutability step 21 of 26
`static mut` is dead: what edition 2024 changed
Drive one process-global counter from four threads, twice, two different ways.
pub fn global_counter(ops: Vec<String>) -> Vec<i64>
pub fn sync_cell_counter(ops: Vec<String>) -> Vec<i64>
Each function owns its own global counter and resets it to zero on entry (the same process runs both, and the tests would otherwise accumulate). Then:
| op | effect |
|---|---|
add <a> <b> <c> <d> |
run one round: four scoped threads, thread i adding its delta 500 times to the shared counter |
read |
append the counter’s current value |
| anything else | ignored |
After the last op, append the final total. Return the appended values. So a
round of add 1 1 1 1 moves the counter by 500 × 4 = 2000 — and only if the
state really is shared and synchronised. Lose updates and the number comes
out low; use four separate counters and it comes out wrong differently.
Both functions must return the same answers.
The instinct, and the refusal
A global mutable counter is the first thing a C programmer reaches for, and the spelling is right there in the language:
static mut COUNTER: i64 = 0;
unsafe {
let slot = &mut COUNTER;
*slot += delta;
}
On edition 2024 that is a hard error, with no flag to turn it off:
error: creating a mutable reference to mutable static
| let slot = &mut COUNTER;
| ^^^^^^^^^^^^ mutable reference to mutable static
|
= note: mutable references to mutable statics are dangerous; it's undefined
behavior if any other pointer to the static is used or if any other
reference is created for the static while the mutable reference lives
= note: `#[deny(static_mut_refs)]` (part of `#[deny(rust_2024_compatibility)]`)
on by default
help: use `&raw mut` instead to create a raw pointer
Note what it is and is not. It has no error code — static_mut_refs is a
lint, denied by default. (You may find E0796 in older material. Run
rustc --explain E0796 and it answers “this error code is no longer emitted
by the compiler.” Do not learn it.)
And note the subtlety the lint encodes, because it explains the whole
design: reading or writing the static directly inside unsafe still
compiles. unsafe { COUNTER = 0; } is fine. unsafe { COUNTER } is fine.
What is banned is taking a reference, because a reference carries the
aliasing promise — and in a program with threads there is no way for you to
keep that promise, and no way for the compiler to check that you did. The
unsafe block was never enough; it just moved the blame.
The migration ladder
Every rung solves the same problem with a different amount of machinery. Climb only as far as you must.
-
Atomics.
static COUNTER: AtomicI64 = AtomicI64::new(0);— nounsafe, no lock,fetch_addis one instruction on every machine you care about. For a counter this is the whole answer, and it is whatglobal_countershould use. (1.95 also addedupdateandtry_updatefor the read-modify-write casesfetch_*does not cover.) -
Mutex/RwLock. Both areconst-constructible now, sostatic TABLE: Mutex<Vec<u8>> = Mutex::new(Vec::new());works directly in astatic. Use when the state is bigger than one integer. -
OnceLock/LazyLock. For state that is expensive to build and never changes afterwards — a compiled table, a parsed configuration. The thread-safe twins of the cells from the previous problem. -
&raw mut STATIC. The sanctioned escape when you genuinely must hand a pointer to C. It is a raw pointer, so it makes no aliasing promise, which is exactly why it is allowed where&mutis not. A tool, not a loophole. -
A hand-rolled
SyncUnsafeCell. Which is the second half of this problem.
Rung five, by hand
struct SyncCell<T>(UnsafeCell<T>);
// SAFETY: every access goes through a function that holds LOCK for the
// duration, so no two threads ever touch the value at once.
unsafe impl<T: Send> Sync for SyncCell<T> {}
static CELL: SyncCell<i64> = SyncCell(UnsafeCell::new(0));
static LOCK: Mutex<()> = Mutex::new(());
A static must be Sync, and UnsafeCell is not — that is the whole reason
you cannot simply put one in a static and be done. unsafe impl Sync is you
promising what the compiler cannot prove, and the Mutex is what makes
the promise true. Every access must hold the lock; a single one that does not
makes the unsafe impl a lie and the program unsound.
This is what std::cell::SyncUnsafeCell would give you, and it is still
unstable on 1.95, which is why you are writing it out.
Two details in the implementation are worth care. The lock guard must be bound
to a name — let _guard = LOCK.lock()…, never let _ = LOCK.lock()…,
which releases immediately and is caught by clippy’s deny-by-default
let_underscore_lock. And UnsafeCell::get() returns a *mut T, so the
dereference is where the unsafe block genuinely belongs.
const is not static, and the difference is stark
Worth burning in while you are here. A const is a value inlined at every
use site; a static is one place in memory.
const C: AtomicUsize = AtomicUsize::new(0);
static S: AtomicUsize = AtomicUsize::new(0);
for _ in 0..3 { print!("{} ", C.fetch_add(1, Ordering::SeqCst)); } // 0 0 0
for _ in 0..3 { print!("{} ", S.fetch_add(1, Ordering::SeqCst)); } // 0 1 2
The const version gives every use its own fresh copy, so it never counts
anything. Three separate diagnostics fire on that one mistake — rustc’s
const_item_interior_mutations plus clippy’s declare_interior_mutable_const
and borrow_interior_mutable_const — which tells you how often people hit it.
Determinism
The threads race on purpose, but nothing you return depends on who wins. Additions commute, the round is joined before anything is read, and the tests assert totals rather than timings. That is the general rule for testing concurrent code: assert on a deterministic artefact — a final count, a sorted merge, an event log — never on a duration or an interleaving.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.