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: initialize shared state once
Fix the declarations in the starter so that all workers share one cell and one initialization counter.
pub fn init_once(workers: usize) -> (u64, usize)
Start workers.max(1) threads. Each calls CELL.get_or_init with a closure
that increments ATTEMPTS and returns 42. Wait for all workers to finish,
then return the stored value and the counter.
The expected result is (42, 1) for every tested worker count, including
zero. Keep the threaded implementation: returning this pair directly would
skip the initialization exercise.
The workers need not arrive simultaneously. Whether they overlap or run one after another, they must observe the same initialized value. The initializer in this exercise does not panic, so it runs once. The static state also persists across repeated calls in the same process.
Why the starter needs static storage
The starter declares CELL and ATTEMPTS as const items. These provide
values at their use sites, rather than shared mutable storage. Updating one
temporary counter does not update a counter read elsewhere. Similarly,
initializing a temporary cell does not initialize the cell used by the final
get(), so the starter's expect panics.
Use static for each shared object. Both constructors can run in a constant
initializer:
static CELL: OnceLock<u64> = OnceLock::new();
static ATTEMPTS: AtomicUsize = AtomicUsize::new(0);
The existing imports supply these types. Clippy's
declare_interior_mutable_const and borrow_interior_mutable_const
diagnostics help identify the original mistake.
Reading and initializing a OnceLock
| Method | Behavior |
|---|---|
get() |
Returns a reference if ready, otherwise None. Never blocks. |
get_or_init(f) |
Initializes with f if needed, or waits for another initializer. Returns a reference to the stored value. |
set(value) |
Tries to store a value. Returns it in Err(value) if already initialized; may wait for an ongoing initialization. |
wait() |
Waits until initialization finishes. Does not start initialization itself. |
Calls made after initialization reuse the value without rerunning a closure. If an initializer panics and unwinds, that panic reaches its caller and the cell remains uninitialized. A later call can try again. Avoid recursively initializing the same cell from its own initializer.
In this exercise, the counter only counts initializer calls. Relaxed atomic
operations are sufficient: OnceLock handles publication of its value, and
leaving thread::scope waits for the workers before the final counter read.
When LazyLock is useful
LazyLock stores its initializer when it is constructed. Dereferencing it
or calling LazyLock::force initializes it on demand:
use std::sync::LazyLock;
static LABEL: LazyLock<String> = LazyLock::new(|| String::from("ready"));
fn main() {
assert_eq!(LABEL.as_str(), "ready");
}
Choose it when one predefined initializer suits the application.
OnceLock lets the caller supply initialization logic or a value later.
Their panic behavior differs. A panic during LazyLock initialization
poisons that instance; later attempts to force its value also panic.
OnceLock allows another attempt after an unwinding initializer panic.
Returning Err is different from panicking. A
LazyLock<Result<T, E>> can cache an error as its initialized value.
Likewise, OnceLock<Result<T, E>>::get_or_init stores an Err; it does
not automatically retry it. Decide explicitly whether failures should be
cached or retried.
Check your understanding How can each thread keep its own RefCell scratch buffer? Show answer Hide answer
Use thread_local! when the buffer should be separate for each thread:
use std::cell::RefCell;
thread_local! {
static SCRATCH: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
fn main() {
SCRATCH.with_borrow_mut(|buf| buf.push(1));
std::thread::spawn(|| {
SCRATCH.with_borrow(|buf| assert!(buf.is_empty()));
}).join().unwrap();
SCRATCH.with_borrow(|buf| assert_eq!(buf.as_slice(), &[1]));
}
Access occurs through a closure, keeping borrows within the thread-local
access. The const initializer can enable more efficient initialization.
This works without sharing the RefCell between threads. It is useful for
scratch space, but does not provide a shared global buffer.
Thread-local destructors have platform-specific limitations, so do not rely on them for cleanup that must always happen at process exit.
A passing submission must compile, pass the tests, and pass
clippy -D warnings.
References: OnceLock, LazyLock, and thread-local storage.
Loading visualization…
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.