Loading pageā¦
Rust walkthroughs
Loading pageā¦
once_cell::sync::OnceCell differ from std::sync::Once for one-time initialization?once_cell::sync::OnceCell and std::sync::Once solve the same problemāensuring code runs exactly onceābut with fundamentally different APIs and capabilities. std::sync::Once is a low-level primitive that only coordinates execution; it doesn't store values, requiring you to manage storage separately with Option or UnsafeCell. OnceCell combines synchronization with storage, providing a type-safe container that holds the initialized value and allows safe access after initialization. This makes OnceCell significantly more ergonomic for the common case of lazy static initialization, while Once remains useful when you need to run side effects without storing a result or need fine-grained control over initialization.
use std::sync::Once;
use std::sync::Mutex;
static START: Once = Once::new();
static mut DATA: Option<Mutex<Vec<String>>> = None;
fn initialize_once() {
// call_once ensures this runs exactly one time
START.call_once(|| {
// Unsafe required because we're modifying static mut
unsafe {
DATA = Some(Mutex::new(vec!["initialized".to_string()]));
}
});
}
fn get_data() -> &'static Mutex<Vec<String>> {
initialize_once();
// Unsafe required to access static mut
unsafe {
DATA.as_ref().unwrap()
}
}
fn main() {
get_data().lock().unwrap().push("item".to_string());
println!("{:?}", get_data().lock().unwrap());
}Once only coordinates execution; you must manage storage yourself with unsafe.
use once_cell::sync::OnceCell;
use std::sync::Mutex;
static DATA: OnceCell<Mutex<Vec<String>>> = OnceCell::new();
fn get_data() -> &'static Mutex<Vec<String>> {
DATA.get_or_init(|| {
Mutex::new(vec!["initialized".to_string()])
})
}
fn main() {
get_data().lock().unwrap().push("item".to_string());
println!("{:?}", get_data().lock().unwrap());
}OnceCell handles both synchronization and storage with no unsafe code.
use std::sync::Once;
use once_cell::sync::OnceCell;
// === std::sync::Once ===
static ONCE: Once = Once::new();
static mut VALUE_ONCE: Option<i32> = None;
fn init_with_once() {
unsafe {
if VALUE_ONCE.is_none() {
ONCE.call_once(|| {
VALUE_ONCE = Some(42);
});
}
let _ = VALUE_ONCE.unwrap();
}
}
// === once_cell::sync::OnceCell ===
static CELL: OnceCell<i32> = OnceCell::new();
fn init_with_cell() {
let value = CELL.get_or_init(|| 42);
println!("Value: {}", value);
}
fn main() {
init_with_once();
init_with_cell();
}OnceCell provides get_or_init for ergonomic lazy initialization.
use std::sync::Once;
use once_cell::sync::OnceCell;
// === With Once, accessing is manual ===
static ONCE: Once = Once::new();
static mut CONFIG_ONCE: Option<String> = None;
fn get_config_once() -> &'static str {
ONCE.call_once(|| {
unsafe {
CONFIG_ONCE = Some("config_value".to_string());
}
});
unsafe { CONFIG_ONCE.as_ref().unwrap() }
}
// === With OnceCell, access is built-in ===
static CONFIG_CELL: OnceCell<String> = OnceCell::new();
fn get_config_cell() -> Option<&'static str> {
CONFIG_CELL.get().map(|s| s.as_str())
}
fn get_or_init_config() -> &'static str {
CONFIG_CELL.get_or_init(|| "config_value".to_string())
}
fn main() {
// Check if initialized
assert!(CONFIG_CELL.get().is_none());
// Initialize
get_or_init_config();
// Now get returns Some
assert!(CONFIG_CELL.get().is_some());
}OnceCell provides get(), get_or_init(), and set() methods.
use std::sync::Once;
use once_cell::sync::OnceCell;
// === With Once, errors are tricky ===
static ONCE: Once = Once::new();
static mut RESULT_ONCE: Option<Result<String, String>> = None;
fn init_with_error_once() -> Result<&'static str, &'static str> {
ONCE.call_once(|| {
unsafe {
// If this fails, we store the error
RESULT_ONCE = Some(Err("init failed".to_string()));
}
});
unsafe {
RESULT_ONCE.as_ref().unwrap().as_ref().map_err(|e| e.as_str())
}
}
// === With OnceCell, get_or_try_init handles errors ===
static CELL: OnceCell<String> = OnceCell::new();
fn init_with_error_cell() -> Result<&'static str, String> {
CELL.get_or_try_init(|| {
// If this returns Err, cell remains uninitialized
Err("init failed".to_string())
}).map(|s| s.as_str())
}
fn successful_init() -> &'static str {
CELL.get_or_try_init(|| Ok("success".to_string()))
.unwrap()
}
fn main() {
// First attempt fails, cell not initialized
assert!(init_with_error_cell().is_err());
// Still uninitialized, can try again
assert!(CELL.get().is_none());
// This succeeds
assert_eq!(successful_init(), "success");
}OnceCell::get_or_try_init allows initialization that can fail, retrying on error.
use std::sync::Once;
use once_cell::sync::OnceCell;
// === With Once, checking status requires separate state ===
static ONCE: Once = Once::new();
static mut INITIALIZED_ONCE: bool = false;
fn is_initialized_once() -> bool {
unsafe { INITIALIZED_ONCE }
}
fn do_init_once() {
ONCE.call_once(|| {
unsafe {
INITIALIZED_ONCE = true;
}
});
}
// === With OnceCell, status is inherent ===
static CELL: OnceCell<String> = OnceCell::new();
fn check_cell() {
if CELL.get().is_some() {
println!("Already initialized");
} else {
println!("Not yet initialized");
}
}
fn main() {
check_cell(); // Not yet initialized
CELL.set("value".to_string()).unwrap();
check_cell(); // Already initialized
}OnceCell naturally tracks initialization state through get().
use std::sync::Once;
use once_cell::sync::OnceCell;
// === Multiple related values with Once ===
static ONCE_A: Once = Once::new();
static ONCE_B: Once = Once::new();
static mut VALUE_A: Option<i32> = None;
static mut VALUE_B: Option<String> = None;
fn init_a() {
ONCE_A.call_once(|| unsafe { VALUE_A = Some(42); });
}
fn init_b() {
ONCE_B.call_once(|| unsafe { VALUE_B = Some("hello".to_string()); });
}
// === Multiple values with OnceCell ===
static CELL_A: OnceCell<i32> = OnceCell::new();
static CELL_B: OnceCell<String> = OnceCell::new();
fn main() {
CELL_A.get_or_init(|| 42);
CELL_B.get_or_init(|| "hello".to_string());
// Much cleaner, no unsafe needed
}OnceCell scales better for multiple independent values.
use std::sync::Once;
use once_cell::sync::OnceCell;
// === Manual lazy static with Once ===
static HASHMAP_ONCE: Once = Once::new();
static mut HASHMAP: Option<std::collections::HashMap<&'static str, i32>> = None;
fn get_hashmap_once() -> &'static std::collections::HashMap<&'static str, i32> {
HASHMAP_ONCE.call_once(|| {
let mut map = std::collections::HashMap::new();
map.insert("one", 1);
map.insert("two", 2);
map.insert("three", 3);
unsafe {
HASHMAP = Some(map);
}
});
unsafe { HASHMAP.as_ref().unwrap() }
}
// === OnceCell makes this trivial ===
use once_cell::sync::Lazy;
static HASHMAP_CELL: Lazy<std::collections::HashMap<&'static str, i32>> = Lazy::new(|| {
let mut map = std::collections::HashMap::new();
map.insert("one", 1);
map.insert("two", 2);
map.insert("three", 3);
map
});
fn main() {
// Access is direct
println!("{:?}", HASHMAP_CELL.get("one"));
// HASHMAP_CELL is initialized on first access
}once_cell::sync::Lazy provides a convenient wrapper for the common pattern.
use once_cell::sync::OnceCell;
use std::cell::OnceCell as StdOnceCell;
// unsync::OnceCell for single-threaded use
fn single_threaded() {
let cell = StdOnceCell::new();
// Can be set once
cell.set(42).ok();
// Cannot be set again
assert!(cell.set(100).is_err());
// Get the value
assert_eq!(cell.get(), Some(&42));
}
// sync::OnceCell for multi-threaded use
fn multi_threaded() {
use std::thread;
static CELL: OnceCell<i32> = OnceCell::new();
let handles: Vec<_> = (0..10)
.map(|i| {
thread::spawn(move || {
// All threads get the same value
CELL.get_or_init(|| {
println!("Initializing from thread {}", i);
i * 10 // First thread wins
})
})
})
.collect();
for h in handles {
println!("Got: {}", h.join().unwrap());
}
}
fn main() {
single_threaded();
multi_threaded();
}OnceCell provides both sync and unsync variants.
use std::sync::Once;
use once_cell::sync::OnceCell;
// === Once is simpler for pure side effects ===
static LOG_INIT: Once = Once::new();
fn init_logging() {
LOG_INIT.call_once(|| {
// Initialize logging system
println!("Initializing logging...");
// This just needs to run once, no value to store
});
}
// With OnceCell, you'd need to store something
static LOG_CELL: OnceCell<()> = OnceCell::new();
fn init_logging_cell() {
LOG_CELL.get_or_init(|| {
println!("Initializing logging...");
// Must return something even though we don't need it
});
}
fn main() {
init_logging();
init_logging(); // Second call does nothing
// Once is cleaner for pure side effects
}Once is cleaner when you only need to run code once without storing a result.
use once_cell::sync::OnceCell;
// OnceCell handles initialization ordering gracefully
static CONFIG: OnceCell<Config> = OnceCell::new();
static DATABASE: OnceCell<Database> = OnceCell::new();
struct Config {
db_url: String,
}
struct Database {
url: String,
}
fn get_config() -> &'static Config {
CONFIG.get_or_init(|| Config {
db_url: "postgres://localhost".to_string(),
})
}
fn get_database() -> &'static Database {
// DATABASE depends on CONFIG
let config = get_config();
DATABASE.get_or_init(|| Database {
url: config.db_url.clone(),
})
}
fn main() {
// Accessing database initializes config first
let db = get_database();
println!("DB URL: {}", db.url);
// Config was initialized as dependency
assert!(CONFIG.get().is_some());
}OnceCell handles initialization dependencies naturally.
use std::sync::Once;
use once_cell::sync::OnceCell;
use std::time::Instant;
fn performance_comparison() {
// Both use similar synchronization primitives internally
// OnceCell adds slight overhead for value storage
// But the ergonomic benefits usually outweigh this
static ONCE: Once = Once::new();
static CELL: OnceCell<i32> = OnceCell::new();
let start = Instant::now();
for _ in 0..1_000_000 {
ONCE.call_once(|| {});
}
let once_time = start.elapsed();
let start = Instant::now();
for _ in 0..1_000_000 {
CELL.get_or_init(|| 42);
}
let cell_time = start.elapsed();
println!("Once: {:?}", once_time);
println!("OnceCell: {:?}", cell_time);
// After initialization, both are fast for repeated access
// OnceCell's get() is lock-free after initialization
}
fn main() {
performance_comparison();
}Both are fast; OnceCell has minimal overhead after initialization.
// As of Rust 1.70+, OnceLock is in std::sync
use std::sync::OnceLock;
static STD_CELL: OnceLock<String> = OnceLock::new();
fn modern_std_once_cell() {
let value = STD_CELL.get_or_init(|| "initialized".to_string());
println!("{}", value);
// API is nearly identical to once_cell::sync::OnceCell
}
// once_cell crate provides additional features:
// - Lazy<T> for inline initialization
// - race::OnceBox for heap-allocated types
// - unsync::OnceCell for single-threaded use
fn main() {
modern_std_once_cell();
}Rust 1.70+ includes OnceLock in std, similar to OnceCell.
| Feature | std::sync::Once | once_cell::sync::OnceCell |
|---------|-------------------|----------------------------|
| Value storage | No (manual) | Yes (built-in) |
| Safe access | No (requires unsafe) | Yes |
| Error handling | Manual | get_or_try_init |
| Status check | Manual | get().is_some() |
| API ergonomics | Low-level | High-level |
| Use case | Side effects | Lazy values |
| Type safety | Manual | Automatic |
std::sync::Once and once_cell::sync::OnceCell serve complementary purposes:
Use std::sync::Once when:
OnceUse OnceCell when:
get_or_try_init)get_or_initKey insight: OnceCell is built on top of synchronization primitives like Once but provides a complete solution that combines coordination with storage. The unsafe code required with Once (managing Option or UnsafeCell) is encapsulated within OnceCell, giving you a safe, ergonomic API. For most use cases involving lazy initialization of values, OnceCell (or std::sync::OnceLock in Rust 1.70+) is the better choice. Reserve Once for cases where you truly only need to run code once without producing a value.