Rust Once & Lazy Values
OnceLock, LazyLock and Once are tools that run building code exactly once, however many threads come asking. Every thread then shares the one result.
OnceLock: the first caller builds it, everyone shares it
get_or_init runs the closure if the slot is empty, and hands every caller a reference to that same value:
use std::sync::OnceLock;
static TABLE: OnceLock<Vec<i32>> = OnceLock::new();
fn table() -> &'static Vec<i32> { TABLE.get_or_init(|| vec![2, 3, 5, 7]) }
fn main() { println!("third prime = {}", table()[2]); println!("same table = {}", std::ptr::eq(table(), table())); }Peek with get, fill with set
Before anyone builds the value, get() honestly reports None:
use std::sync::OnceLock;
static NAME: OnceLock<String> = OnceLock::new();
fn main() { println!("before = {:?}", NAME.get()); NAME.set(String::from("Ferris")).unwrap(); println!("after = {:?}", NAME.get()); }LazyLock: the building code sits with the value
LazyLock keeps the closure in the definition itself. Nothing runs until the first use โ the first deref โ so callers need no setup code at all:
static GREETING: LazyLock<String> = LazyLock::new(|| {
format!("hello from {}", "one lazy value")
});
// nothing has run yet โ the closure fires on the first deref:
println!("{}", *GREETING);Once:: call_once for one-time actions
Sometimes there is no value to store. There is just an action that must happen a single time. Then the bare Once is the tool underneath:
static SETUP: Once = Once::new();
SETUP.call_once(|| {
// runs on exactly one thread, exactly once;
// every other call waits for it, then returns
});Example
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::OnceLock;
use std::thread;
static SETTINGS: OnceLock<Vec<(&'static str, i32)>> = OnceLock::new();
static BUILDS: AtomicUsize = AtomicUsize::new(0);
fn settings() -> &'static Vec<(&'static str, i32)> {
SETTINGS.get_or_init(|| {
BUILDS.fetch_add(1, Ordering::SeqCst);
vec![("retries", 3), ("timeout", 30)]
})
}
fn main() {
thread::scope(|s| {
for _ in 0..4 {
s.spawn(|| {
let table = settings();
assert_eq!(table.len(), 2);
});
}
});
println!("threads used the table = 4");
println!("times the table was built = {}", BUILDS.load(Ordering::SeqCst));
println!("timeout setting = {}", settings()[1].1);
}Key points
- Build once, share everywhere: the first thread builds the value, and every later thread uses that same one.
- OnceLock<T> is a slot any thread may fill exactly once.
- get_or_init(closure) builds the value if the slot is empty.
- It hands everyone the same &T.
- While one thread is inside the building closure, other threads asking for the value wait.
- Nobody can observe a half-built value.
- Use set(value) to fill the slot yourself.
- It returns Err if the slot was already taken.
- Your value is handed back to you.
- LazyLock<T> (in std since Rust 1.80) puts the building closure in the definition.
- The value then appears on first use.
- Reach for OnceLock when the building needs arguments known only at run time.
- If the building closure panics, a OnceLock slot stays empty.
- The next caller tries again.
- A panicked Once is poisoned.
- Later call_once calls return without running the closure.
- Code in the wild still uses them.
- They carry the same idea: build once, on first use, safely across threads.
- Settings tables, costly lookup maps, and shared configuration are examples of values built exactly once.
- OnceCell is the single-threaded member of this family.
In this example, four threads race into settings() at the same moment. The first to arrive runs the closure: it bumps the BUILDS counter and builds the two-entry table. The other three wait inside get_or_init until the value exists. Then everyone receives a reference to that very same table. main receives one too, afterwards. So the printed lines are certain. 4 threads used the table. The table was built exactly 1 time. The timeout entry reads 30. Without the OnceLock, protecting this race would take a Mutex. It would lock on every lookup, forever. And it would guard a value that only ever gets built once.
Note
Before Rust 1.80 this family lived in the once_cell and lazy_static crates.
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท Rust Playground
Exercise
Five threads call get_or_init at the same moment on an empty OnceLock. How many times does the closure run?