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
Implement the same shared counter twice: once with an atomic integer and
once with a hand-written synchronized cell. Both counters are static
items, so each one is a single value shared by the whole process.
pub fn global_counter(ops: Vec<String>) -> Vec<i64>
pub fn sync_cell_counter(ops: Vec<String>) -> Vec<i64>
Each function uses its own static counter and sets it to zero when it starts. A static keeps its value between calls, so without the reset a second call in the same process would start from whatever the previous call left behind; the contract is that every call starts from zero. The functions are not required to work if two calls run at the same time.
Process the operations in order. Each operation is split on whitespace, and matching is case-sensitive:
| operation | effect |
|---|---|
add <a> <b> <c> <d> (exactly four values) |
start four scoped threads; thread i adds its value to the shared counter 500 times, one addition at a time; wait for all four to finish |
read (no extra words) |
append the counter's current value to the output |
| anything else | ignored, including add with a different number of values |
A value in an add line that does not parse as an i64 (for example x)
counts as 0. After the last operation, append the counter's value once
more, so the output always has at least one element. For an empty operation
list the answer is [0].
So add 1 1 1 1 moves the counter by 4 × 500 × 1 = 2000, and
add 1 2 3 4 moves it by 500 × (1 + 2 + 3 + 4) = 5000. You get these
totals only if all four threads update the same counter and no update is
lost. The tests use small values, so no partial total can overflow i64;
overflow behavior is not part of the contract.
Both functions must return the same values. The tests check only the returned values, so they cannot tell which mechanism you used. The sections below explain the intended one for each function.
Why static mut with a reference is rejected
A static item is one location in memory that lives for the whole program.
A static mut is a static that code may modify. It still exists in edition
2024, and accessing it still requires an unsafe block. The obvious way to
update one looks like this:
static mut COUNTER: i64 = 0;
fn bump(delta: i64) {
unsafe {
let slot = &mut COUNTER;
*slot += delta;
}
}
On edition 2024, rustc 1.98.1 rejects it:
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
This error comes from a lint, static_mut_refs, not from a hard language
rule, so it has no E error code. The lint is a warning on earlier editions
and deny-by-default on edition 2024. Because it is a lint, #[allow(static_mut_refs)]
makes the code compile again; that silences the check but does not make the
code correct. Older material may mention error code E0796;
rustc --explain E0796 now says that code is no longer emitted.
The lint targets references specifically. These still compile inside
unsafe, because they read or write the static directly without creating a
reference:
unsafe {
COUNTER = 0;
COUNTER += 2;
let current = COUNTER; // copies the i64 out
}
Creating a shared reference (&COUNTER) is reported too, and so is a method
call that borrows the static implicitly, such as LIST.push(1) on a
static mut LIST: Vec<i32>.
The reason is the promise a reference makes. A &mut T promises that nothing
else reads or writes that value while the reference is in use, and a &T
promises that nothing writes it. Every function that can name a static mut
can reach it, from any thread, so the compiler cannot check that promise. If the promise is broken, the behavior is undefined, even in a single
thread. The unsafe block is your statement that you have checked it
yourself, and for a counter updated from four threads that check would fail:
the direct COUNTER += delta form compiles, but four threads running it at
once is a data race, which is also undefined behavior.
Safer replacements, from simplest to most manual
- Atomics.
static COUNTER: AtomicI64 = AtomicI64::new(0);needs nounsafe, no lock and nomut.fetch_addperforms the read, the addition and the write as one indivisible operation, so no update is lost. For a single counter this is the natural choice, and it is whatglobal_countershould use. For other read-modify-write steps there arefetch_updateand the newerupdateandtry_updatemethods.AtomicI64exists only on targets that support 64-bit atomic operations. Mutex/RwLock.Mutex::newandRwLock::newareconst fn, sostatic TABLE: Mutex<Vec<u8>> = Mutex::new(Vec::new());works directly. Use a lock when the shared state is more than one integer or when several values must change together.OnceLock/LazyLock. For state that is built once and then only read, such as a parsed configuration.OnceLockis initialized by the first call to a method likeget_or_init;LazyLockruns its initializer on first access.&raw mut STATIC. Creates a raw pointer (*mut T) to astatic mutwithout creating a reference, which is what the lint'shelpline suggests. Taking the pointer is safe; reading or writing through it needsunsafe, and you still must prevent data races yourself. One common use is passing the address to foreign (for example C) code.- A hand-written synchronized cell. This is the second half of the exercise.
A synchronized cell by hand
UnsafeCell<T> is the standard library's basic building block for interior
mutability: UnsafeCell::get() returns a raw *mut T to its contents, and
dereferencing that pointer is unsafe. It is the type that Cell,
RefCell, Mutex and the atomics are built on.
use std::cell::UnsafeCell;
use std::sync::Mutex;
struct SyncCell<T>(UnsafeCell<T>);
// SAFETY: every access to CELL goes through one function that holds LOCK
// for the whole access, so no two threads use the value at the same time.
unsafe impl<T: Send> Sync for SyncCell<T> {}
static CELL: SyncCell<i64> = SyncCell(UnsafeCell::new(0));
static LOCK: Mutex<()> = Mutex::new(());
The type of a (non-mut) static must implement Sync, because every
thread can reach it. UnsafeCell<T> does not implement Sync, so
static CELL: UnsafeCell<i64> fails to compile with E0277. Writing
unsafe impl Sync is a promise the compiler cannot check, and the Mutex
is what keeps that promise. This Sync implementation would be unsound for
arbitrary uses of SyncCell; it is acceptable here only because the field is
private to this module and every access to CELL holds LOCK. One access
that skips the lock would allow a data race, which is undefined behavior.
The standard library has std::cell::SyncUnsafeCell for this purpose, but
it is still unstable on Rust 1.98.1, so this exercise writes the wrapper by
hand.
Two details matter in the implementation:
- Bind the lock guard to a name:
let _guard = LOCK.lock().... The patternlet _ = LOCK.lock();drops the guard, and so releases the lock, immediately. rustc rejects that pattern with the deny-by-defaultlet_underscore_locklint. UnsafeCell::get()itself is safe. Theunsafeblock belongs around the dereference of the*mut Tit returns, while the guard is alive.
const is not static
A const is a value that is copied into every place it is used. A static
is one location in memory.
use std::sync::atomic::{AtomicUsize, Ordering};
const C: AtomicUsize = AtomicUsize::new(0);
static S: AtomicUsize = AtomicUsize::new(0);
fn main() {
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
}
Each use of C creates a fresh atomic that starts at zero and is thrown away
after the call, so it never counts. rustc warns about this with
const_item_interior_mutations, and Clippy adds
declare_interior_mutable_const and borrow_interior_mutable_const. A
shared counter must be a static.
Why the output is deterministic
The four threads in a round run in an unpredictable order, but the result
does not depend on that order. Integer addition gives the same total in any
order (when nothing overflows), and std::thread::scope waits for every
thread it spawned before it returns, so each read happens after the whole
round has finished. The tests compare totals, not timings or interleavings.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.