Loading page…
Rust walkthroughs
Loading page…
once_cell::sync::OnceCell differ from std::sync::OnceLock introduced in Rust 1.70?once_cell::sync::OnceCell and std::sync::OnceLock provide identical functionality—thread-safe one-time initialization of a value—but differ in availability, API naming, and feature scope. OnceLock is the standard library's adoption of the OnceCell pattern, stabilized in Rust 1.70 after years of the once_cell crate serving this need. The once_cell crate offers additional APIs beyond what's in std, including Lazy<T> for deferred initialization with a closure, race::OnceCell for non-thread-safe scenarios, and the sync::Lazy type. For projects on Rust 1.70+, OnceLock is the idiomatic choice for standard use cases, while once_cell remains valuable for additional features or supporting older Rust versions.
// Using once_cell crate
use once_cell::sync::OnceCell;
static INSTANCE: OnceCell<String> = OnceCell::new();
fn main() {
// Get or initialize
let value = INSTANCE.get_or_init(|| {
println!("Initializing...");
String::from("Hello, World!")
});
println!("{}", value);
// Subsequent calls return existing value
let value2 = INSTANCE.get_or_init(|| {
panic!("This won't be called");
});
println!("{}", value2);
}OnceCell provides thread-safe lazy initialization.
// Using std (Rust 1.70+)
use std::sync::OnceLock;
static INSTANCE: OnceLock<String> = OnceLock::new();
fn main() {
// Get or initialize
let value = INSTANCE.get_or_init(|| {
println!("Initializing...");
String::from("Hello, World!")
});
println!("{}", value);
// Subsequent calls return existing value
let value2 = INSTANCE.get_or_init(|| {
panic!("This won't be called");
});
println!("{}", value2);
}OnceLock provides identical functionality with different naming.
use once_cell::sync::OnceCell;
use std::sync::OnceLock;
fn main() {
// OnceCell API
let cell: OnceCell<i32> = OnceCell::new();
cell.get(); // Option<&T>
cell.get_or_init(|| 42); // &T
cell.get_mut(); // Option<&mut T>
cell.get_unchecked(); // &T (unsafe)
cell.set(42); // Result<(), T>
cell.take(); // Option<T>
cell.into_inner(); // Option<T>
// OnceLock API (same functionality, different names)
let lock: OnceLock<i32> = OnceLock::new();
lock.get(); // Option<&T>
lock.get_or_init(|| 42); // &T
lock.get_mut(); // Option<&mut T>
lock.get_unchecked(); // &T (unsafe)
lock.set(42); // Result<(), T>
lock.take(); // Option<T>
lock.into_inner(); // Option<T>
// APIs are essentially identical
}Both provide the same core operations with equivalent semantics.
// once_cell provides Lazy<T> for deferred initialization
use once_cell::sync::Lazy;
static CONFIG: Lazy<Vec<String>> = Lazy::new(|| {
let mut config = Vec::new();
config.push("setting1".to_string());
config.push("setting2".to_string());
config
});
fn main() {
// CONFIG initializes on first access
println!("Before access");
println!("Config: {:?}", *CONFIG); // Initializes here
println!("After access");
// std doesn't have Lazy<T>
// Workaround in std:
use std::sync::OnceLock;
static CONFIG_STD: OnceLock<Vec<String>> = OnceLock::new();
// Manual get_or_init each time
let config = CONFIG_STD.get_or_init(|| {
vec!["setting1".to_string(), "setting2".to_string()]
});
}Lazy<T> provides automatic initialization; OnceLock requires explicit get_or_init.
use once_cell::sync::Lazy;
use std::sync::OnceLock;
// once_cell::Lazy - declarative, automatic
static DB_POOL: Lazy<DbPool> = Lazy::new(|| {
DbPool::connect("localhost:5432")
});
// std::sync::OnceLock - explicit initialization
static DB_POOL_STD: OnceLock<DbPool> = OnceLock::new();
fn get_pool() -> &'static DbPool {
DB_POOL_STD.get_or_init(|| DbPool::connect("localhost:5432"))
}
struct DbPool;
impl DbPool {
fn connect(_addr: &str) -> Self { DbPool }
}
fn main() {
// Lazy: access directly
let _pool = &*DB_POOL;
// OnceLock: use helper or get_or_init each time
let _pool = get_pool();
}Lazy is more ergonomic for static lazy values.
// once_cell provides unsync (single-threaded) variants
use once_cell::unsync::OnceCell as UnsyncOnceCell;
use once_cell::unsync::Lazy as UnsyncLazy;
fn main() {
// Single-threaded OnceCell (no synchronization overhead)
let cell: UnsyncOnceCell<String> = UnsyncOnceCell::new();
cell.set("value".to_string()).unwrap();
// Single-threaded Lazy
let lazy: UnsyncLazy<Vec<i32>> = UnsyncLazy::new(|| {
vec![1, 2, 3]
});
println!("{:?}", *lazy);
// std::sync::OnceLock is always thread-safe
// No unsync variant in std (use Cell<Option<T>> or similar)
}once_cell::unsync provides single-threaded variants without atomics overhead.
use once_cell::sync::OnceCell;
use std::sync::OnceLock;
fn main() {
// Both support try_insert (nightly/unstable in std)
let cell: OnceCell<i32> = OnceCell::new();
// once_cell: try_insert is stable
match cell.try_insert(42) {
Ok(&value) => println!("Inserted: {}", value),
Err((_current, attempted)) => {
println!("Already set to {}, tried {}", _current, attempted);
}
}
// std::sync::OnceLock::set provides similar functionality
let lock: OnceLock<i32> = OnceLock::new();
match lock.set(42) {
Ok(()) => println!("Set successfully"),
Err(_) => println!("Already set"),
}
}Both support checking if already initialized when setting.
use std::sync::OnceLock;
use once_cell::sync::OnceCell;
use std::thread;
fn main() {
// Both use similar internal synchronization
// OnceLock uses std::sync::Once internally
// OnceCell uses a custom lock-free implementation
static CELL: OnceCell<i32> = OnceCell::new();
static LOCK: OnceLock<i32> = OnceLock::new();
let h1 = thread::spawn(|| {
CELL.get_or_init(|| {
println!("OnceCell initializing");
42
})
});
let h2 = thread::spawn(|| {
LOCK.get_or_init(|| {
println!("OnceLock initializing");
42
})
});
h1.join().unwrap();
h2.join().unwrap();
// Both guarantee:
// 1. Initialization happens exactly once
// 2. All threads see the initialized value
// 3. Initialization is thread-safe
}Both provide the same thread-safety guarantees.
// Before: using once_cell
use once_cell::sync::OnceCell;
static OLD_STYLE: OnceCell<String> = OnceCell::new();
// After: using std
use std::sync::OnceLock;
static NEW_STYLE: OnceLock<String> = OnceLock::new();
// Migration is straightforward:
// 1. Change import
// 2. Change type name
// 3. API is compatible
// For Lazy, you need to keep using once_cell or create wrapper
use once_cell::sync::Lazy; // Keep this
static STILL_NEEDED: Lazy<String> = Lazy::new(|| "value".to_string());
// Or create your own Lazy wrapper around OnceLock:
struct MyLazy<T> {
cell: OnceLock<T>,
init: fn() -> T,
}
impl<T> MyLazy<T> {
fn new(init: fn() -> T) -> Self {
MyLazy { cell: OnceLock::new(), init }
}
fn get(&self) -> &T {
self.cell.get_or_init(|| (self.init)())
}
}
impl<T: 'static> std::ops::Deref for MyLazy<T> {
type Target = T;
fn deref(&self) -> &T {
self.get()
}
}Migration is simple except for Lazy which requires additional work.
use once_cell::sync::OnceCell;
use std::sync::OnceLock;
use std::time::Instant;
fn main() {
// Both have similar performance characteristics
// First access: initialization cost + synchronization
// Subsequent accesses: fast read (atomic check)
const ITERATIONS: u32 = 1_000_000;
static CELL: OnceCell<i32> = OnceCell::new();
static LOCK: OnceLock<i32> = OnceLock::new();
// Initialize both
CELL.get_or_init(|| 42);
LOCK.get_or_init(|| 42);
// Benchmark reads
let start = Instant::now();
for _ in 0..ITERATIONS {
let _ = CELL.get();
}
let cell_time = start.elapsed();
let start = Instant::now();
for _ in 0..ITERATIONS {
let _ = LOCK.get();
}
let lock_time = start.elapsed();
println!("OnceCell: {:?}", cell_time);
println!("OnceLock: {:?}", lock_time);
// Performance is nearly identical
}Both have minimal overhead after initialization.
// Use std::sync::OnceLock when:
// 1. You're on Rust 1.70+
// 2. You only need basic one-time initialization
// 3. You want to avoid external dependencies
// 4. Standard library is preferred
static CONFIG: OnceLock<Config> = OnceLock::new();
// Use once_cell::sync::OnceCell when:
// 1. Supporting Rust < 1.70
// 2. You need Lazy<T>
// 3. You need unsync variants
// 4. You need race::OnceCell (no interior mutability)
// Use once_cell::sync::Lazy when:
// 1. You want declarative static initialization
// 2. You want automatic initialization on access
// 3. The initialization closure captures environment
static COMPLEX: Lazy<Vec<String>> = Lazy::new(|| {
let mut v = Vec::new();
for i in 0..100 {
v.push(format!("item_{}", i));
}
v
});Choose based on Rust version and feature needs.
// once_cell provides race module for non-blocking variants
use once_cell::race;
fn main() {
// race::OnceBox: for boxed values, no interior mutability
static BOXED: race::OnceBox<String> = race::OnceBox::new();
BOXED.set(Box::new("value".to_string())).unwrap();
// race::OnceRef: for references
static REF: race::OnceRef<str> = race::OnceRef::new();
REF.set("value").unwrap();
// std doesn't have these specialized variants
// They're useful when you need:
// - Non-blocking reads after initialization
// - No interior mutability guarantees
// - Specialized memory management
}The race module provides specialized non-blocking variants.
use once_cell::sync::{OnceCell, Lazy};
use once_cell::unsync::{OnceCell as UnsyncOnceCell, Lazy as UnsyncLazy};
use once_cell::race::{OnceBox, OnceRef};
use std::sync::OnceLock;
fn main() {
// === std::sync::OnceLock ===
// Thread-safe one-time initialization
// Stable in Rust 1.70+
// Methods: new, get, get_or_init, get_mut, get_unchecked, set, take, into_inner
// === once_cell::sync::OnceCell ===
// Identical to OnceLock
// Available in older Rust versions
// Same methods as OnceLock
// === once_cell::sync::Lazy ===
// Thread-safe automatic initialization
// NO EQUIVALENT IN STD
// Implements Deref for automatic access
// === once_cell::unsync::OnceCell ===
// Single-threaded, no atomics
// NO EQUIVALENT IN STD
// Use Cell<Option<T>> for similar (but different) behavior
// === once_cell::unsync::Lazy ===
// Single-threaded automatic initialization
// NO EQUIVALENT IN STD
// === once_cell::race::OnceBox ===
// Non-blocking, for boxed values
// NO EQUIVALENT IN STD
// === once_cell::race::OnceRef ===
// Non-blocking, for references
// NO EQUIVALENT IN STD
}once_cell provides more variants than std.
Core functionality: OnceCell and OnceLock provide identical thread-safe one-time initialization with equivalent APIs.
Availability:
OnceLock: Standard library, Rust 1.70+OnceCell: External crate, works on older Rust versionsAPI differences: Essentially none—same methods, same semantics, different names.
once_cell advantages:
Lazy<T>: Automatic initialization on access (no explicit get_or_init)unsync module: Single-threaded variants without atomics overheadrace module: Non-blocking variants for specialized use casesOnceLock advantages:
Migration path: For basic use, replace OnceCell with OnceLock by changing imports and type names. For Lazy, either keep using once_cell or implement your own wrapper.
Key insight: OnceLock standardizes the most common use case of OnceCell—thread-safe one-time initialization. The once_cell crate remains valuable for its additional types (Lazy, unsync variants, race module) that aren't in std. For simple lazy statics on Rust 1.70+, OnceLock is the right choice. For declarative initialization or single-threaded scenarios, once_cell provides ergonomics that std doesn't yet match.