OnceLock and Lazy Initialization
Some values are expensive to build and should only be built if something actually asks for them: a parsed config, a compiled regex, a lookup table. A static cannot help, because a static initializer has to be a constant expression evaluated at compile time.
OnceLock<T> is the answer. It is a slot that starts empty and can be filled exactly once:
static SETTINGS: OnceLock<Settings> = OnceLock::new();
fn settings() -> &'static Settings {
SETTINGS.get_or_init(build_settings)
}get_or_init is where the concurrency lives. If ten threads call it at the same moment on an empty lock, exactly one runs build_settings. The other nine block until it finishes and then get a reference to that same value. The initializer never runs twice, and no caller ever sees a half-built value.
Because the value can never be replaced, handing out &'static T is sound, and callers need no lock at all to read it.
LazyLock<T> is the same machinery with the initializer stored inside, so it can be used like a normal static:
static TABLE: LazyLock<Vec<u64>> =
LazyLock::new(|| expensive());Reach for LazyLock when the initializer is a self-contained closure, and OnceLock when the value comes from somewhere the static cannot see, such as a command-line argument set during startup.
Your Task
Fill in three items. Settings and the BUILD_COUNT counter are given to you.
pub struct Settings {
pub name: String,
pub retries: u32,
}build_settings() is also given. It bumps BUILD_COUNT and returns Settings { name: "rustfinity", retries: 3 }. Do not change it: the tests use that counter to prove your initializer runs exactly once.
The storage slot
Declare a static OnceLock<Settings> for settings() to fill.
Implement settings
pub fn settings() -> &'static SettingsReturns the shared settings, building them with build_settings on the first call only. Every call, from every thread, must return a reference to the same value.
Implement build_count
pub fn build_count() -> usizeReads BUILD_COUNT. After any number of settings() calls from any number of threads, this must be 1.
Declare TRIANGULAR
pub static TRIANGULAR: LazyLock<Vec<u64>>The first 32 triangular numbers, computed on first use: 0, 1, 3, 6, 10, 15, ..., where entry n is the sum of every integer from 0 to n.
assert_eq!(TRIANGULAR[..6], [0, 1, 3, 6, 10, 15]);
assert_eq!(TRIANGULAR.len(), 32);Notes
OnceLock::new()is aconst fn, which is what lets it appear in astatic.- There is a closed form for the nth triangular number, but a running sum works just as well.
LazyLock<Vec<u64>>derefs toVec<u64>, soTRIANGULAR[3]andTRIANGULAR.len()work without any extra unwrapping.
Hints
Click here to reveal hints
- The slot is
static SETTINGS: OnceLock<Settings> = OnceLock::new();. get_or_inittakes the initializer as a function, soSETTINGS.get_or_init(build_settings)is the whole body ofsettings. No parentheses afterbuild_settings: you are passing the function, not calling it.build_countis oneload(Ordering::Relaxed)on the counter.- For the table:
(0..32).map(|n: u64| n * (n + 1) / 2).collect(). - If the compiler complains that the closure type does not match, remember
LazyLock::newwants a closure taking no arguments.
Run to execute your code and see test results here.