Loading pageā¦
Rust walkthroughs
Loading pageā¦
once_cell::sync::OnceLock::get_or_init ensure thread-safe one-time initialization without blocking?OnceLock::get_or_init uses an atomic state machine to coordinate initialization across threads, allowing concurrent readers to proceed without blocking while ensuring exactly one thread performs initialization. The implementation relies on a three-state atomic flagāuninitialized, initializing, and initializedācombined with careful memory ordering to provide safe concurrent access. Unlike std::sync::Once which blocks threads during initialization, OnceLock allows threads to read already-initialized values immediately and uses std::sync::Once internally only for the initialization coordination. This design minimizes contention after initialization completes, making subsequent reads essentially lock-free.
use once_cell::sync::OnceLock;
use std::collections::HashMap;
static CONFIG: OnceLock<HashMap<String, String>> = OnceLock::new();
fn get_config() -> &'static HashMap<String, String> {
CONFIG.get_or_init(|| {
println!("Initializing config...");
let mut config = HashMap::new();
config.insert("host".to_string(), "localhost".to_string());
config.insert("port".to_string(), "8080".to_string());
config
})
}
fn main() {
let config = get_config();
println!("Host: {}", config.get("host").unwrap());
let config2 = get_config();
println!("Port: {}", config2.get("port").unwrap());
// "Initializing config..." prints only once
// Second call returns existing reference
}get_or_init ensures initialization runs exactly once, returning a reference to the stored value.
use once_cell::sync::OnceLock;
use std::sync::atomic::{AtomicU8, Ordering};
// Conceptual model: OnceLock uses atomic state tracking
// State values:
// 0 = UNINITIALIZED - no value stored, initialization not started
// 1 = INITIALIZING - initialization in progress
// 2 = INITIALIZED - value stored and ready
struct ConceptualOnceLock<T> {
state: AtomicU8,
value: std::mem::MaybeUninit<T>,
// Internal: std::sync::Once for blocking coordination
}
impl<T> ConceptualOnceLock<T> {
fn get_or_init<F>(&self, f: F) -> &T
where
F: FnOnce() -> T,
{
loop {
// Fast path: already initialized
let state = self.state.load(Ordering::Acquire);
if state == 2 {
// SAFETY: Initialized state guarantees value is stored
unsafe {
return &*self.value.as_ptr();
}
}
// Try to claim initialization
if self.state.compare_exchange(
0, // UNINITIALIZED
1, // INITIALIZING
Ordering::AcqRel,
Ordering::Acquire,
).is_ok() {
// We won the race - initialize
let value = f();
unsafe {
self.value.as_mut_ptr().write(value);
}
self.state.store(2, Ordering::Release);
return unsafe { &*self.value.as_ptr() };
}
// State is INITIALIZING - need to wait
// Real implementation uses std::sync::Once for blocking
}
}
}
// The actual OnceLock is more sophisticated:
// - Uses std::sync::Once for blocking during initialization
// - Avoids spinning with efficient blocking primitives
// - Handles panic during initializationThe atomic state ensures exactly one thread performs initialization while others wait or read the completed value.
use once_cell::sync::OnceLock;
use std::sync::Arc;
use std::thread;
static CACHE: OnceLock<Vec<u64>> = OnceLock::new();
fn main() {
// Initialize once
CACHE.get_or_init(|| {
println!("Computing expensive cache...");
(0..1000).collect()
});
// Now spawn threads that read
let handles: Vec<_> = (0..10)
.map(|_| {
thread::spawn(|| {
// This is essentially lock-free!
// Just an atomic load and pointer dereference
let cache = CACHE.get().unwrap();
println!("Thread read cache length: {}", cache.len());
})
})
.collect();
for handle in handles {
handle.join().unwrap();
}
// After initialization, get_or_init is also non-blocking:
// It checks the atomic state first (Acquire load)
// If INITIALIZED, returns reference immediately
}Once initialized, all accesses are fast atomic reads with no synchronization overhead.
use once_cell::sync::OnceLock;
use std::sync::Arc;
use std::thread;
static DATA: OnceLock<String> = OnceLock::new();
fn main() {
let handles: Vec<_> = (0..5)
.map(|i| {
thread::spawn(move || {
let value = DATA.get_or_init(|| {
println!("Thread {} initializing", i);
// Simulate expensive initialization
thread::sleep(std::time::Duration::from_millis(100));
format!("Initialized by thread {}", i)
});
value
})
})
.collect();
for handle in handles {
let result = handle.join().unwrap();
println!("Got: {}", result);
}
// Output:
// "Thread X initializing" (exactly once)
// "Got: Initialized by thread X" (5 times, same value)
// Only one thread performs initialization
// Others block briefly until complete
}Multiple threads racing to initialize results in exactly one winner; others wait for completion.
use once_cell::sync::OnceLock;
use std::cell::UnsafeCell;
// OnceLock provides safe interior mutability for one-time initialization
// The UnsafeCell is only written during initialization, after which it's immutable
struct LazyCache<T> {
cell: OnceLock<T>,
}
impl<T> LazyCache<T> {
fn new() -> Self {
LazyCache {
cell: OnceLock::new(),
}
}
fn get_or_init<F>(&self, f: F) -> &T
where
F: FnOnce() -> T,
{
self.cell.get_or_init(f)
}
}
// Usage: immutable interface with lazy interior mutation
fn main() {
let cache: LazyCache<Vec<i32>> = LazyCache::new();
// cache is immutable, but initialization mutates internal state
let data = cache.get_or_init(|| {
println!("Computing...");
vec
![1, 2, 3, 4, 5]
});
let data2 = cache.get_or_init(|| {
println!("This won't print");
vec
![999]; // Never called
});
assert_eq!(data, data2); // Same reference
}OnceLock enables lazy initialization through an immutable interface, hiding the interior mutability.
use once_cell::sync::OnceLock;
use std::panic;
use std::thread;
static DATA: OnceLock<String> = OnceLock::new();
fn main() {
// First attempt: panic
let result1 = panic::catch_unwind(|| {
DATA.get_or_init(|| {
println!("First initialization attempt");
panic!("Initialization failed!");
})
});
assert!(result1.is_err());
// After panic, OnceLock allows retry
// Internal state reset to UNINITIALIZED
let result2 = DATA.get_or_init(|| {
println!("Second initialization attempt");
"Success".to_string()
});
println!("Got: {}", result2);
// Note: The actual behavior depends on OnceLock version
// Some versions "poison" after panic
// Check your version's documentation
}If initialization panics, OnceLock typically allows retryāthe state remains uninitialized.
use once_cell::sync::OnceLock;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
static READY: OnceLock<bool> = OnceLock::new();
static COUNTER: AtomicUsize = AtomicUsize::new(0);
fn writer_thread() {
// Initialize data
COUNTER.store(42, Ordering::Relaxed);
READY.get_or_init(|| true);
}
fn reader_thread() {
// Wait for initialization
READY.get_or_init(|| true);
// Memory ordering guarantees:
// - We see the initialized value
// - We also see all writes that happened before initialization
// - COUNTER is guaranteed to be 42 (with proper ordering)
let value = COUNTER.load(Ordering::Relaxed);
println!("Counter: {}", value);
}
fn main() {
thread::scope(|s| {
s.spawn(writer_thread);
s.spawn(reader_thread);
});
}get_or_init provides Acquire-Release semantics, ensuring visibility of pre-initialization writes.
use once_cell::sync::OnceLock;
use std::sync::Once;
use std::cell::UnsafeCell;
// std::sync::Once approach
static mut ONCE_VALUE: UnsafeCell<Option<String>> = UnsafeCell::new(None);
static ONCE: Once = Once::new();
fn get_with_once() -> &'static str {
ONCE.call_once(|| {
// SAFETY: Single-threaded initialization guaranteed by Once
unsafe {
(*ONCE_VALUE.get()) = Some("initialized".to_string());
}
});
// SAFETY: After call_once returns, value is initialized
unsafe {
(*ONCE_VALUE.get()).as_ref().unwrap()
}
}
// OnceLock approach: much cleaner
static LOCK_VALUE: OnceLock<String> = OnceLock::new();
fn get_with_lock() -> &'static str {
LOCK_VALUE.get_or_init(|| "initialized".to_string())
}
fn main() {
println!("Once: {}", get_with_once());
println!("Lock: {}", get_with_lock());
}
// Key differences:
// 1. OnceLock returns a reference directly
// 2. OnceLock stores the value, Once doesn't
// 3. OnceLock requires no unsafe code
// 4. OnceLock has better ergonomicsOnceLock combines Once's synchronization with storage, eliminating unsafe code.
// lazy_static! macro approach
use lazy_static::lazy_static;
lazy_static! {
static ref LAZY_MAP: std::collections::HashMap<String, i32> = {
let mut map = std::collections::HashMap::new();
map.insert("a".to_string(), 1);
map
};
}
// OnceLock approach
use once_cell::sync::OnceLock;
use std::collections::HashMap;
static ONCE_MAP: OnceLock<HashMap<String, i32>> = OnceLock::new();
fn get_map() -> &'static HashMap<String, i32> {
ONCE_MAP.get_or_init(|| {
let mut map = HashMap::new();
map.insert("a".to_string(), 1);
map
})
}
fn main() {
// Both provide lazy initialization
println!("lazy_static: {:?}", *LAZY_MAP);
println!("OnceLock: {:?}", get_map());
// Differences:
// - OnceLock is in std (Rust 1.70+)
) as part of lazy_static crate
// - OnceLock has simpler API
// - lazy_static supports more complex initialization patterns
// - OnceLock has slightly better performance
}OnceLock is simpler and now in std; lazy_static! offers more macro features.
use once_cell::sync::OnceLock;
use once_cell::unsync::OnceCell;
// Sync version: thread-safe, can be static
static GLOBAL: OnceLock<String> = OnceLock::new();
// Unsync version: not thread-safe, for local use
// Only `unsync::OnceCell`, not `OnceLock`
fn main() {
// OnceLock: thread-safe
let sync_cell: OnceLock<i32> = OnceLock::new();
sync_cell.get_or_init(|| 42);
// OnceCell: not thread-safe, but lighter weight
let unsync_cell: once_cell::unsync::OnceCell<i32> = OnceCell::new();
unsync_cell.get_or_init(|| 42);
// Use OnceLock for:
// - Static variables
// - Shared state across threads
// - Lazy static initialization
// Use OnceCell for:
// - Local variables
// - Single-threaded contexts
// - Slightly better performance when sync not needed
}sync::OnceLock is thread-safe; unsync::OnceCell avoids synchronization overhead when not needed.
use once_cell::sync::OnceLock;
use std::sync::Arc;
struct DatabasePool {
connections: Vec<String>,
}
impl DatabasePool {
fn new() -> Self {
println!("Creating database pool...");
DatabasePool {
connections: vec
!["conn1", "conn2", "conn3"]
,
}
}
fn get_connection(&self) -> &str {
self.connections.first().unwrap()
}
}
static DB_POOL: OnceLock<Arc<DatabasePool>> = OnceLock::new();
fn get_db() -> Arc<DatabasePool> {
Arc::clone(DB_POOL.get_or_init(|| {
Arc::new(DatabasePool::new())
}))
}
fn main() {
// Multiple functions can access the same pool
fn handler1() {
let db = get_db();
println!("Handler1: {}", db.get_connection());
}
fn handler2() {
let db = get_db();
println!("Handler2: {}", db.get_connection());
}
handler1();
handler2();
// "Creating database pool..." prints only once
// Both handlers share the same pool instance
}OnceLock is ideal for global state that needs lazy, thread-safe initialization.
use once_cell::sync::OnceLock;
use std::thread;
use std::time::Instant;
static SLOW: OnceLock<String> = OnceLock::new();
fn main() {
let start = Instant::now();
// Thread 1: Slow initialization
let h1 = thread::spawn(|| {
SLOW.get_or_init(|| {
println!("Thread 1: Starting slow init");
thread::sleep(std::time::Duration::from_millis(100));
println!("Thread 1: Finished slow init");
"slow data".to_string()
})
});
// Thread 2: Tries to read during Thread 1's initialization
let h2 = thread::spawn(|| {
thread::sleep(std::time::Duration::from_millis(50));
println!("Thread 2: Attempting to read");
let value = SLOW.get_or_init(|| {
println!("Thread 2: Initializing (won't run)");
"thread 2 data".to_string()
});
println!("Thread 2: Got '{}'", value);
value
});
h1.join().unwrap();
h2.join().unwrap();
println!("Total time: {:?}", start.elapsed());
}
// Thread 2 blocks efficiently (uses OS synchronization)
// It doesn't spin-wait for the initialization
// Once initialization completes, Thread 2 proceeds immediatelyDuring initialization, waiting threads block efficiently using std::sync::Once internals, not spinning.
use once_cell::sync::OnceLock;
static MAYBE_VALUE: OnceLock<String> = OnceLock::new();
fn main() {
// get(): Returns reference if initialized, None if not
let before = MAYBE_VALUE.get();
println!("Before: {:?}", before); // None
// get_or_init(): Initializes if needed, always returns reference
let value = MAYBE_VALUE.get_or_init(|| "initialized".to_string());
println!("After init: {}", value);
// Now get() returns Some
let after = MAYBE_VALUE.get();
println!("After: {:?}", after); // Some("initialized")
// get_mut(): Returns mutable reference if exclusively held
if let Some(inner) = MAYBE_VALUE.get_mut() {
inner.push(" modified");
}
// get() now shows the modification
println!("Modified: {:?}", MAYBE_VALUE.get());
}
// Use get() when:
// - You want to check if initialized without triggering init
// - You have a separate initialization path
// - You want Option for conditional access
// Use get_or_init() when:
// - You want guaranteed initialization
// - You have a closure to provide the value
// - You want a reference unconditionallyget() returns Option<&T>; get_or_init() guarantees a &T by initializing if needed.
use once_cell::sync::OnceLock;
fn main() {
let cell: OnceLock<Vec<i32>> = OnceLock::new();
// Initialize
cell.get_or_init(|| vec
![1, 2, 3]);
// get() returns reference
let reference: &Vec<i32> = cell.get().unwrap();
println!("Reference: {:?}", reference);
// into_value() consumes OnceLock and returns ownership
// Only available on owned OnceLock, not static
let owned: Vec<i32> = cell.into_value().unwrap();
println!("Owned: {:?}", owned);
// Now cell is consumed, can't use it anymore
// into_value() fails if not initialized
}into_value() extracts the initialized value, consuming the OnceLock and transferring ownership.
use once_cell::sync::OnceLock;
use std::collections::HashMap;
use std::env;
struct Config {
settings: HashMap<String, String>,
debug: bool,
}
impl Config {
fn from_env() -> Self {
let mut settings = HashMap::new();
// Read from environment or files
if let Ok(val) = env::var("APP_HOST") {
settings.insert("host".to_string(), val);
} else {
settings.insert("host".to_string(), "localhost".to_string());
}
if let Ok(val) = env::var("APP_PORT") {
settings.insert("port".to_string(), val);
} else {
settings.insert("port".to_string(), "8080".to_string());
}
let debug = env::var("APP_DEBUG").is_ok();
Config { settings, debug }
}
}
static CONFIG: OnceLock<Config> = OnceLock::new();
fn config() -> &'static Config {
CONFIG.get_or_init(Config::from_env)
}
fn main() {
// Configuration loaded on first access
let cfg = config();
println!("Host: {:?}", cfg.settings.get("host"));
println!("Debug: {}", cfg.debug);
// Subsequent calls return same reference
let cfg2 = config();
assert!(std::ptr::eq(cfg, cfg2));
}Lazy configuration loading with OnceLock ensures single initialization and thread-safe access.
use once_cell::sync::OnceLock;
use std::collections::HashSet;
struct WordDictionary {
words: HashSet<&'static str>,
}
impl WordDictionary {
fn new() -> Self {
println!("Building word dictionary...");
let words: HashSet<&'static str> = include_str!("words.txt")
.lines()
.collect();
println!("Dictionary has {} words", words.len());
WordDictionary { words }
}
fn contains(&self, word: &str) -> bool {
self.words.contains(word)
}
}
static WORDS: OnceLock<WordDictionary> = OnceLock::new();
fn is_valid_word(word: &str) -> bool {
WORDS.get_or_init(WordDictionary::new).contains(word)
}
fn main() {
// Dictionary built on first call
println!("Valid: {}", is_valid_word("hello"));
// Subsequent calls use cached dictionary
println!("Valid: {}", is_valid_word("world"));
println!("Valid: {}", is_valid_word("rust"));
}Expensive computations can be deferred and cached with OnceLock.
Thread-safety mechanism:
// State machine:
// UNINITIALIZED -> INITIALIZING -> INITIALIZED
// | | |
// v v v
// try_init wait/return return ref
// | | |
// +--init------->+ |
// +--complete---->+
// get_or_init flow:
// 1. Atomic load (Acquire) of state
// 2. If INITIALIZED: return reference immediately
// 3. If UNINITIALIZED: try compare_exchange to INITIALIZING
// - Success: run init closure, store value, set INITIALIZED
// - Failure: another thread won, block until INITIALIZED
// 4. Block using std::sync::Once until completeKey characteristics:
| Aspect | Behavior | |--------|----------| | Initialization | Exactly once, thread-safe | | Post-init reads | Lock-free atomic load | | During init | Efficient blocking, not spinning | | Panic handling | State reset, allows retry | | Memory ordering | Acquire-Release semantics |
Performance profile:
When to use:
When alternatives might be better:
unsync::OnceCell)RwLock or Mutex)Key insight: OnceLock::get_or_init achieves thread-safe initialization through a combination of atomic state checking for the fast path and std::sync::Once for the slow path. After initialization completes, every subsequent call performs just an atomic load followed by a pointer dereferenceāessentially lock-free. This design amortizes the synchronization cost: you pay for coordination only during the one-time initialization, after which reads are as fast as possible. The API is safe, requires no unsafe code from users, and integrates naturally with Rust's ownership system through lifetime-qualified references.