456: `OnceLock` and `OnceCell` — Once-Initialized Values
Tutorial
The Problem
Some values are expensive to initialize (parse config, compile regex, connect to DB) and should only be initialized once. Race conditions arise if multiple threads try to initialize simultaneously. OnceLock<T> provides thread-safe once-initialization: get_or_init(|| expensive_computation()) guarantees the closure runs exactly once, even under concurrent access. OnceCell<T> is the single-threaded version. Both are simpler and more ergonomic than the lazy_static! macro they largely replace.
OnceLock is used for global singletons, global configuration, compiled regex caches, connection pools initialized once, and any value needing lazy evaluation with thread-safe initialization guarantee.
🎯 Learning Outcomes
OnceLock<T> (thread-safe) and OnceCell<T> (single-thread)get_or_init guarantees exactly-once initialization under concurrent accessstatic CONFIG: OnceLock<HashMap<...>> creates a global lazy singletonOnceLock is the modern replacement for lazy_static! for most use casesOnceCell suffices (no concurrent access to the cell itself)Code Example
#![allow(clippy::all)]
// 456. OnceLock and OnceCell for lazy init
use std::cell::OnceCell;
use std::collections::HashMap;
use std::sync::OnceLock;
static CONFIG: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static GREETING: OnceLock<String> = OnceLock::new();
fn config() -> &'static HashMap<&'static str, &'static str> {
CONFIG.get_or_init(|| {
println!("init config");
[("host", "localhost"), ("port", "8080")]
.iter()
.cloned()
.collect()
})
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::OnceLock;
#[test]
fn test_once_only() {
let lock: OnceLock<u32> = OnceLock::new();
let n = AtomicU32::new(0);
lock.get_or_init(|| {
n.fetch_add(1, Ordering::SeqCst);
42
});
lock.get_or_init(|| {
n.fetch_add(1, Ordering::SeqCst);
99
});
assert_eq!(*lock.get().unwrap(), 42);
assert_eq!(n.load(Ordering::SeqCst), 1);
}
}Key Differences
lazy is a language keyword; Rust's OnceLock is a standard library type.static OnceLock enables lazy global initialization; OCaml let global_val = lazy (...) achieves the same at module level.OnceLock handles panics in get_or_init by allowing retry; OCaml's Lazy.force re-raises the exception on retry.once_cell crate**: Before stabilization in std, the once_cell crate provided Lazy<T> which initializes on first deref — slightly more ergonomic than OnceLock.OCaml Approach
OCaml's Lazy.t is the standard lazy value: let config = lazy (make_config ()) and Lazy.force config for access. Lazy.t is thread-safe in OCaml 5.x with the standard library's guarantee of single evaluation. In OCaml 4.x, Lazy.t uses a mutex internally for thread safety. The Lazy.from_val function creates an already-evaluated lazy value.
Full Source
#![allow(clippy::all)]
// 456. OnceLock and OnceCell for lazy init
use std::cell::OnceCell;
use std::collections::HashMap;
use std::sync::OnceLock;
static CONFIG: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static GREETING: OnceLock<String> = OnceLock::new();
fn config() -> &'static HashMap<&'static str, &'static str> {
CONFIG.get_or_init(|| {
println!("init config");
[("host", "localhost"), ("port", "8080")]
.iter()
.cloned()
.collect()
})
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::OnceLock;
#[test]
fn test_once_only() {
let lock: OnceLock<u32> = OnceLock::new();
let n = AtomicU32::new(0);
lock.get_or_init(|| {
n.fetch_add(1, Ordering::SeqCst);
42
});
lock.get_or_init(|| {
n.fetch_add(1, Ordering::SeqCst);
99
});
assert_eq!(*lock.get().unwrap(), 42);
assert_eq!(n.load(Ordering::SeqCst), 1);
}
}#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::OnceLock;
#[test]
fn test_once_only() {
let lock: OnceLock<u32> = OnceLock::new();
let n = AtomicU32::new(0);
lock.get_or_init(|| {
n.fetch_add(1, Ordering::SeqCst);
42
});
lock.get_or_init(|| {
n.fetch_add(1, Ordering::SeqCst);
99
});
assert_eq!(*lock.get().unwrap(), 42);
assert_eq!(n.load(Ordering::SeqCst), 1);
}
}
Exercises
static LOGGER: OnceLock<Logger> where Logger reads its configuration from environment variables. Verify that the environment is read only once and subsequent calls use the cached configuration.fn is_valid_ipv4(s: &str) -> bool using static RE: OnceLock<Regex>. Verify with 10 concurrent threads calling the function that the regex is compiled exactly once.OnceCell<Box<dyn Database>> in a struct AppState to enable injecting different database implementations in tests vs. production, initializing once via set from the setup code.