๐ŸŽ“ A1Learner
๐Ÿ”

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?