Loading page…
Rust walkthroughs
Loading page…
once_cell::sync::OnceCell and std::sync::OnceLock for one-time initialization?once_cell::sync::OnceCell and std::sync::OnceLock provide thread-safe one-time initialization, but differ in API surface, feature set, and ecosystem integration. OnceLock is the standard library's stabilized subset of OnceCell functionality—available in Rust 1.70+ without external dependencies. OnceCell from the once_cell crate offers additional methods like get_mut, get_unchecked, take, and into_inner that enable mutable access and value extraction. The trade-off is between dependency-free standard library availability (OnceLock) versus richer API and backward compatibility (OnceCell).
// Problem: Initialize a value exactly once, thread-safely
// Multiple threads may attempt initialization
// Only one should succeed; others should see the initialized value
// Without synchronization (BUGGY):
static mut VALUE: Option<String> = None;
fn get_value() -> &'static str {
unsafe {
if VALUE.is_none() {
VALUE = Some("initialized".to_string()); // DATA RACE!
}
VALUE.as_ref().unwrap()
}
}
// With OnceLock/OnceCell (CORRECT):
use std::sync::OnceLock;
static VALUE: OnceLock<String> = OnceLock::new();
fn get_value() -> &'static str {
VALUE.get_or_init(|| "initialized".to_string())
}One-time initialization ensures a value is computed exactly once and safely accessed from multiple threads.
use std::sync::OnceLock;
static CONFIG: OnceLock<Config> = OnceLock::new();
#[derive(Debug)]
struct Config {
name: String,
value: i32,
}
fn get_config() -> &'static Config {
CONFIG.get_or_init(|| {
// This closure runs exactly once
Config {
name: "default".to_string(),
value: 42,
}
})
}
fn main() {
println!("{:?}", get_config());
println!("{:?}", get_config()); // Uses cached value
}OnceLock::get_or_init initializes the value on first access and returns a reference.
use once_cell::sync::OnceCell;
static CONFIG: OnceCell<Config> = OnceCell::new();
#[derive(Debug)]
struct Config {
name: String,
value: i32,
}
fn get_config() -> &'static Config {
CONFIG.get_or_init(|| {
Config {
name: "default".to_string(),
value: 42,
}
})
}
fn main() {
println!("{:?}", get_config());
}OnceCell::get_or_init works identically to OnceLock for basic initialization.
use std::sync::OnceLock;
use once_cell::sync::OnceCell;
// Both provide:
// - new() -> Self
// - get(&self) -> Option<&T>
// - set(&self, value: T) -> Result<(), T>
// - get_or_init<F: FnOnce() -> T>(&self, f: F) -> &T
// - get_or_try_init<F: FnOnce() -> Result<T, E>>(&self, f: F) -> Result<&T, E>
fn core_api_comparison() {
// Initialization
let lock: OnceLock<String> = OnceLock::new();
let cell: OnceCell<String> = OnceCell::new();
// Get value (returns None if uninitialized)
assert!(lock.get().is_none());
assert!(cell.get().is_none());
// Set value (returns Err if already set)
assert!(lock.set("value".to_string()).is_ok());
assert!(cell.set("value".to_string()).is_ok());
// Get initialized value
assert!(lock.get().is_some());
assert!(cell.get().is_some());
// get_or_init
let lock2: OnceLock<i32> = OnceLock::new();
let cell2: OnceCell<i32> = OnceCell::new();
assert_eq!(*lock2.get_or_init(|| 42), 42);
assert_eq!(*cell2.get_or_init(|| 42), 42);
}Both types share the same core API for getting and setting values.
use once_cell::sync::OnceCell;
fn oncecell_exclusive_methods() {
let cell: OnceCell<String> = OnceCell::new();
cell.set("value".to_string()).unwrap();
// get_mut: Get mutable reference (requires exclusive access)
if let Some(value) = cell.get_mut() {
value.push_str("_modified");
}
// take: Remove and return the value
let value = cell.take();
assert_eq!(value, Some("value_modified".to_string()));
assert!(cell.get().is_none()); // Now empty
// into_inner: Consume and return the value
cell.set("new_value".to_string()).unwrap();
let inner = cell.into_inner();
assert_eq!(inner, Some("new_value".to_string()));
}OnceCell provides get_mut, take, and into_inner for mutable access and value extraction.
use once_cell::sync::OnceCell;
fn get_mut_example() {
let mut cell: OnceCell<Vec<i32>> = OnceCell::new();
cell.set(vec![1, 2, 3]).unwrap();
// Requires &mut self, so exclusive access is guaranteed
if let Some(vec) = cell.get_mut() {
vec.push(4);
vec.push(5);
}
assert_eq!(cell.get(), Some(&vec![1, 2, 3, 4, 5]));
// OnceLock does NOT have get_mut
// Standard library chose minimal API
}get_mut allows modifying the value with exclusive access—OnceLock doesn't support this.
use once_cell::sync::OnceCell;
fn take_example() {
let cell: OnceCell<String> = OnceCell::new();
cell.set("value".to_string()).unwrap();
// Remove and return the value
let value = cell.take();
assert_eq!(value, Some("value".to_string()));
// Cell is now empty
assert!(cell.get().is_none());
// Can set again
cell.set("new_value".to_string()).unwrap();
assert_eq!(cell.get(), Some(&"new_value".to_string()));
}take removes and returns the value, resetting the cell—useful for re-initialization.
use once_cell::sync::OnceCell;
fn into_inner_example() {
let cell: OnceCell<String> = OnceCell::new();
cell.set("value".to_string()).unwrap();
// Consume the cell and get the inner value
let inner = cell.into_inner();
assert_eq!(inner, Some("value".to_string()));
// cell is now consumed and inaccessible
// With uninitialized cell:
let empty: OnceCell<String> = OnceCell::new();
assert_eq!(empty.into_inner(), None);
}into_inner consumes the cell and returns its contents—useful for cleanup.
use std::sync::OnceLock;
fn standard_library_benefits() {
// No external dependency required
// Available in Rust 1.70+
// Consistent with std types
static GLOBAL: OnceLock<String> = OnceLock::new();
// Works with all std synchronization primitives
use std::sync::Mutex;
static COMPLEX: OnceLock<Mutex<Vec<i32>>> = OnceLock::new();
let guard = COMPLEX.get_or_init(|| Mutex::new(vec![1, 2, 3])).lock().unwrap();
println!("{:?}", *guard);
}OnceLock is built into the standard library—no dependency needed.
// once_cell::sync::OnceCell
// - Works on older Rust versions (1.36+ for sync module)
// - Requires external dependency
// - Richer API (get_mut, take, into_inner)
// - Unstable features behind feature flags
// std::sync::OnceLock
// - Requires Rust 1.70+
// - No external dependency
// - Minimal API (core functionality only)
// - Always stable, no feature flags
// Migration path:
// - New projects: use OnceLock for simpler cases
// - Need get_mut/take/into_inner: use OnceCell
// - Support older Rust: use OnceCellChoose based on Rust version requirements and API needs.
use std::sync::OnceLock;
use once_cell::sync::OnceCell;
// Static usage (both work identically)
static STATIC_LOCK: OnceLock<String> = OnceLock::new();
static STATIC_CELL: OnceCell<String> = OnceCell::new();
fn static_usage() {
STATIC_LOCK.get_or_init(|| "lock".to_string());
STATIC_CELL.get_or_init(|| "cell".to_string());
}
// Instance usage
struct Config {
name: OnceLock<String>,
value: OnceLock<i32>,
}
impl Config {
fn new() -> Self {
Self {
name: OnceLock::new(),
value: OnceLock::new(),
}
}
fn get_name(&self) -> &str {
self.name.get_or_init(|| "default".to_string())
}
}
// OnceCell allows reconfiguration via take
struct ReconfigurableConfig {
name: OnceCell<String>,
}
impl ReconfigurableConfig {
fn reconfigure(&mut self, new_name: String) {
// OnceCell: can take and set again
self.name.take();
self.name.set(new_name).ok();
}
}Both work for static and instance usage; OnceCell supports reconfiguration patterns.
use std::sync::OnceLock;
use std::thread;
fn thread_safety() {
static VALUE: OnceLock<i32> = OnceLock::new();
let handles: Vec<_> = (0..10)
.map(|i| {
thread::spawn(move || {
// All threads see the same value
// Only one initialization happens
let value = VALUE.get_or_init(|| {
println!("Thread {} initializing", i);
i // Only one thread's value wins
});
*value
})
})
.collect();
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
// All results are the same (whichever thread won the race)
assert!(results.iter().all(|&r| r == results[0]));
}Both OnceLock and OnceCell use atomic operations for thread-safe initialization.
use std::sync::OnceLock;
use once_cell::sync::OnceCell;
// Both use similar internal implementation:
// - Atomic flag for initialization state
// - Spin lock or blocking during initialization
// - No overhead after initialization (direct pointer access)
// The implementations are nearly identical in performance
// Differences are in API surface, not runtime behavior
// Memory layout comparison:
// OnceLock<T>: stores T directly + state flag
// OnceCell<T>: stores Option<T> + state flag
fn performance_notes() {
// After initialization, get() is a simple atomic load
// No synchronization overhead for reads
// During initialization, threads may spin or block
// Implementation depends on std version and once_cell version
// Both are optimized for the "already initialized" case
// Initialization overhead is amortized across all accesses
}Both types have near-identical performance; differences are in API, not implementation.
use std::sync::OnceLock;
// Pattern for lazy initialization
struct Database {
connection: String,
}
impl Database {
fn connect() -> Self {
println!("Connecting...");
Self {
connection: "connected".to_string(),
}
}
}
fn lazy_pattern() {
static DB: OnceLock<Database> = OnceLock::new();
// Database::connect() only called on first access
let db = DB.get_or_init(|| Database::connect());
println!("{}", db.connection);
}
// Compare with lazy_static! macro (external crate)
// lazy_static! creates similar behavior with macro syntax
// OnceLock/OnceCell use explicit initializationBoth types support the lazy initialization pattern common in Rust applications.
use std::sync::OnceLock;
use std::sync::Mutex;
// get_or_try_init for fallible initialization
fn fallible_init() -> Result<&'static String, &'static str> {
static VALUE: OnceLock<Result<String, String>> = OnceLock::new();
let result = VALUE.get_or_init(|| {
// Simulate fallible initialization
let config = std::env::var("CONFIG_PATH");
match config {
Ok(path) => Ok(format!("Loaded from {}", path)),
Err(_) => Err("CONFIG_PATH not set".to_string()),
}
});
// Can't easily retry with different result
// OnceLock stores whatever was first computed
result.as_ref().map_err(|e| e.as_str())
}
// Better pattern for fallible initialization:
struct LazyResult<T, E> {
result: OnceLock<Result<T, E>>,
}
impl<T, E> LazyResult<T, E> {
fn get_or_try_init<F>(&self, f: F) -> Result<&T, &E>
where
F: FnOnce() -> Result<T, E>,
{
match self.result.get() {
Some(Ok(value)) => Ok(value),
Some(Err(error)) => Err(error),
None => {
let result = f();
self.result.set(result).ok();
self.get_or_try_init(|| panic!("already set"))
}
}
}
}Both support get_or_try_init for fallible initialization.
// Both crates also offer unsynchronized variants:
use once_cell::unsync::OnceCell as UnsyncOnceCell;
// std::sync::OnceLock has no unsync variant in std
fn unsync_example() {
let cell: UnsyncOnceCell<i32> = UnsyncOnceCell::new();
cell.set(42).unwrap();
assert_eq!(cell.get(), Some(&42));
// Unsynchronized variant is not thread-safe
// Use only in single-threaded contexts
// Lower overhead than sync version
}
// std doesn't provide an unsync OnceLock
// Use once_cell::unsync::OnceCell for that caseonce_cell provides an unsynchronized variant; std does not.
| Feature | OnceLock (std) | OnceCell (once_cell) |
|---------|------------------|------------------------|
| new() | ✓ | ✓ |
| get() | ✓ | ✓ |
| set() | ✓ | ✓ |
| get_or_init() | ✓ | ✓ |
| get_or_try_init() | ✓ | ✓ |
| get_mut() | ✗ | ✓ |
| take() | ✗ | ✓ |
| into_inner() | ✗ | ✓ |
| get_unchecked() | ✗ | ✓ (unsafe) |
| Unsynchronized variant | ✗ | ✓ (unsync::OnceCell) |
| External dependency | ✗ | ✓ |
| Minimum Rust version | 1.70+ | 1.36+ (sync), 1.31+ (unsync) |
// Migration from once_cell to std::sync::OnceLock
// Before (once_cell):
use once_cell::sync::OnceCell;
static CACHE: OnceCell<String> = OnceCell::new();
fn get_cache() -> &'static String {
CACHE.get_or_init(|| "cached".to_string())
}
// After (std):
use std::sync::OnceLock;
static CACHE: OnceLock<String> = OnceLock::new();
fn get_cache() -> &'static String {
CACHE.get_or_init(|| "cached".to_string())
}
// But if you use OnceCell-exclusive features:
// You MUST keep using once_cell
fn needs_oncecell() {
use once_cell::sync::OnceCell;
let mut cell: OnceCell<String> = OnceCell::new();
cell.set("value".to_string()).unwrap();
// OnceLock doesn't have get_mut
cell.get_mut().map(|s| s.push_str("_modified"));
// OnceLock doesn't have take
let value = cell.take();
println!("{:?}", value);
// Must stay with OnceCell for these operations
}Migration is straightforward for basic usage; OnceCell is required for advanced patterns.
use std::sync::OnceLock;
use std::env;
struct AppConfig {
database_url: String,
api_key: String,
debug_mode: bool,
}
impl AppConfig {
fn load() -> Self {
Self {
database_url: env::var("DATABASE_URL")
.unwrap_or_else(|_| "localhost:5432".to_string()),
api_key: env::var("API_KEY")
.expect("API_KEY must be set"),
debug_mode: env::var("DEBUG")
.map(|v| v == "true")
.unwrap_or(false),
}
}
}
static CONFIG: OnceLock<AppConfig> = OnceLock::new();
fn config() -> &'static AppConfig {
CONFIG.get_or_init(AppConfig::load)
}
fn main() {
// Configuration loaded once on first access
println!("DB: {}", config().database_url);
println!("Debug: {}", config().debug_mode);
}Singleton pattern with OnceLock ensures configuration loads exactly once.
use once_cell::sync::OnceCell;
use std::collections::HashMap;
use std::sync::Mutex;
type PluginFactory = fn() -> Box<dyn Plugin>;
trait Plugin {
fn name(&self) -> &str;
fn execute(&self);
}
static PLUGINS: OnceCell<Mutex<HashMap<String, PluginFactory>>> = OnceCell::new();
fn register_plugin(name: &str, factory: PluginFactory) {
let plugins = PLUGINS.get_or_init(|| Mutex::new(HashMap::new()));
plugins.lock().unwrap().insert(name.to_string(), factory);
}
fn create_plugin(name: &str) -> Option<Box<dyn Plugin>> {
let plugins = PLUGINS.get_or_init(|| Mutex::new(HashMap::new()));
plugins.lock().unwrap().get(name).map(|f| f())
}
// Plugins can be registered at startup
// Registry is initialized on first registration or useOnceCell enables lazy initialization of mutable registries.
use once_cell::sync::OnceCell;
use std::time::Instant;
struct Cache<T> {
value: T,
timestamp: Instant,
ttl_secs: u64,
}
impl<T: Clone> Cache<T> {
fn new(value: T, ttl_secs: u64) -> Self {
Self {
value,
timestamp: Instant::now(),
ttl_secs,
}
}
fn is_valid(&self) -> bool {
Instant::now().duration_since(self.timestamp).as_secs() < self.ttl_secs
}
}
struct CachedValue<T> {
cell: OnceCell<Cache<T>>,
compute: fn() -> T,
ttl_secs: u64,
}
impl<T: Clone> CachedValue<T> {
fn new(compute: fn() -> T, ttl_secs: u64) -> Self {
Self {
cell: OnceCell::new(),
compute,
ttl_secs,
}
}
fn get(&self) -> T {
// Check if cache is valid
if let Some(cache) = self.cell.get() {
if cache.is_valid() {
return cache.value.clone();
}
}
// Recompute (OnceCell doesn't allow re-setting, so we need get_mut)
// This requires &mut self, or we use a different pattern
self.cell.get_or_init(|| {
Cache::new((self.compute)(), self.ttl_secs)
}).value.clone()
}
// With get_mut, we can invalidate:
fn invalidate(&mut self) {
self.cell.take();
}
}
// OnceLock can't support invalidate pattern without external mutexOnceCell's take method enables cache invalidation patterns that OnceLock cannot support.
use once_cell::sync::OnceCell;
use std::sync::Mutex;
// Global state for testing
static TEST_CONFIG: OnceCell<Mutex<String>> = OnceCell::new();
fn setup_test_config() {
TEST_CONFIG.get_or_init(|| Mutex::new("default".to_string()));
}
fn get_test_config() -> String {
TEST_CONFIG.get()
.expect("Config not initialized")
.lock()
.unwrap()
.clone()
}
// In tests, we can reset the config:
#[cfg(test)]
fn reset_test_config() {
// Only works with OnceCell, not OnceLock
TEST_CONFIG.take();
}
#[test]
fn test_something() {
setup_test_config();
// ... test code ...
reset_test_config(); // Clean up for other tests
}take enables test isolation with shared static state.
Decision guide:
| Requirement | Choose |
|-------------|--------|
| No external dependencies | OnceLock |
| Rust 1.70+ only | OnceLock |
| Need get_mut | OnceCell |
| Need take (reset) | OnceCell |
| Need into_inner | OnceCell |
| Support Rust < 1.70 | OnceCell |
| Need unsynchronized variant | OnceCell (unsync) |
| Simple lazy initialization | Either (prefer OnceLock) |
| Static singleton | Either (prefer OnceLock) |
| Reconfigurable static | OnceCell |
| Test isolation | OnceCell |
Key insight: std::sync::OnceLock provides a dependency-free, standard library implementation for the common case of one-time initialization with immutable access. It's the right choice for most new code targeting Rust 1.70+. once_cell::sync::OnceCell extends this with methods for mutable access (get_mut), value extraction (take, into_inner), and unsafe unchecked access (get_unchecked), enabling patterns like cache invalidation, reconfiguration, and test isolation that require resetting or consuming the stored value. The trade-off is accepting an external dependency. For code that needs these features, OnceCell remains essential; for simpler lazy initialization, OnceLock is the modern, dependency-free choice. The once_cell crate also provides an unsynchronized variant (unsync::OnceCell) for single-threaded contexts, which has no standard library equivalent.