We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Atomics, Send/Sync and the Memory Model step 8 of 12
OnceLock: one initialisation, however many threads race for it
Have workers threads all race to initialise the same global, and prove that
exactly one of them won.
pub fn init_once(workers: usize) -> (u64, usize)
Two statics:
static CELL: OnceLock<u64> = OnceLock::new();
static ATTEMPTS: AtomicUsize = AtomicUsize::new(0);
Every worker calls CELL.get_or_init(|| { ATTEMPTS.fetch_add(1, Relaxed); 42 }).
Return (*CELL.get().unwrap(), ATTEMPTS.load(Relaxed)). Treat workers == 0
as 1.
The answer is (42, 1) for every worker count, and that is the whole
point: a deterministic assertion over a genuinely racy execution. Sixteen
threads all arrive at the same line at the same instant, and the initialiser
body runs exactly once. Not “almost always once” — once, guaranteed by the
type.
That framing is worth stealing. When a piece of concurrent code has a property that holds regardless of scheduling, assert the property, and you have a test that means something.
OnceLock<T>
fn get(&self) -> Option<&T>
fn set(&self, value: T) -> Result<(), T> // Err gives your value back
fn get_or_init<F: FnOnce() -> T>(&self, f: F) -> &T
fn wait(&self) -> &T // block until initialised
All of these take &self — no mut anywhere — so a OnceLock works
perfectly as a static. Internally it is a Once plus an UnsafeCell: the
first caller into get_or_init runs the closure while every other caller
blocks, and when it finishes they all get a shared reference to the one
value.
This is the correct answer to “I need a global that is expensive to build”,
and it replaces lazy_static! and once_cell::sync::OnceCell outright. It
landed in std in 1.70.
OnceLock never poisons. If the initialiser panics, the cell is simply
still empty and the next caller may try again.
LazyLock<T, F> is not the same, and the difference is nasty
LazyLock (1.80) is OnceLock plus the initialiser, run on first Deref:
static CONFIG: LazyLock<Config> = LazyLock::new(|| load_config());
// CONFIG.timeout <- initialises here, first time only
It is more ergonomic and you should usually prefer it. But say the asymmetry out loud, because people assume the pair behave alike:
A panic in a LazyLock initialiser poisons it irrecoverably. Not “until
cleared” — there is no clear_poison, there is no retry. Every subsequent
access panics for the lifetime of the process. OnceLock in the same
situation is merely still empty.
So if your initialiser can fail, OnceLock with an explicit retry is the
honest choice, and LazyLock is for initialisers that cannot fail.
The starter’s bug is a const
warning: a `const` item should not be interior mutable
[clippy::declare_interior_mutable_const]
A const is not a variable — it is a value inlined at every use site. So
const CELL: OnceLock<u64> creates a fresh, empty cell everywhere CELL
appears. Nothing is ever shared. get_or_init initialises a temporary that is
discarded on the next line, CELL.get() is forever None, and the counter
never leaves zero.
A static is one object, at one address, that everything refers to. That is
what you need, and it works with no ceremony because OnceLock::new and
AtomicUsize::new are const fn — the same reason
static M: Mutex<Vec<u8>> = Mutex::new(Vec::new()) needs no macro.
Clippy flags the declaration and the borrow separately, and rustc has its own
const_item_interior_mutations. Several independent lints agreeing is a
strong signal that the code is not merely unidiomatic.
::: question You need a RefCell<Vec<u8>> scratch buffer in a threaded program. RefCell is not Sync, so it cannot be a static. What now?
thread_local!, which is the honest answer to “how do I use a non-Sync
type in a threaded program”.
thread_local! {
static SCRATCH: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
SCRATCH.with_borrow_mut(|buf| buf.push(1));
Each thread gets its own instance, created on first access and dropped
when the thread ends. Nothing is shared, so Sync is never required — the
problem is dissolved rather than solved.
Two practical notes. Access goes through LocalKey::with (or with_borrow /
with_borrow_mut for a RefCell), because handing out a plain &'static
reference would outlive the thread. And write the initialiser as
const { ... } when you can: it makes access cheaper by removing the
lazy-initialisation check, and clippy::missing_const_for_thread_local is a
warn-by-default performance lint that will tell you so.
The trade-off is the obvious one — per-thread state is invisible to other threads, so it suits caches, buffers and scratch space, not anything that must be observed globally. :::
A note on real-world context
clippy::non_std_lazy_statics flags lazy_static! and once_cell in code
that could use std. It is pedantic, and since this grader has no external
crates it cannot fire here at all. Mentioned because you will meet both crates
in existing code, and the migration to OnceLock/LazyLock is usually
mechanical.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.