Loading page…
Rust walkthroughs
Loading page…
Rust provides several primitives for one-time initialization — ensuring code runs exactly once, even in multi-threaded contexts. These are essential for lazy initialization, singletons, and setup code.
Key types in std::sync:
Once — Low-level primitive for running a closure exactly onceOnceLock<T> — Thread-safe container for one-time initialization of a value (stable since Rust 1.70)LazyLock<T> — Combines OnceLock with a initialization function (stable since Rust 1.80)For single-threaded code:
std::cell::OnceCell<T> — Single-threaded version of OnceLockThese primitives guarantee:
use std::sync::Once;
use std::thread;
static INIT: Once = Once::new();
fn expensive_setup() {
println!("Running expensive setup...");
std::thread::sleep(std::time::Duration::from_millis(100));
println!("Setup complete!");
}
fn main() {
let mut handles = vec![];
// Spawn 5 threads that all need the setup
for i in 0..5 {
let handle = thread::spawn(move || {
// call_once ensures this runs exactly once
INIT.call_once(|| {
expensive_setup();
});
println!("Thread {} proceeding after setup", i);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}use std::sync::OnceLock;
use std::thread;
// Global configuration loaded once
static CONFIG: OnceLock<String> = OnceLock::new();
fn get_config() -> &'static String {
CONFIG.get_or_init(|| {
println!("Loading config...");
// Simulate expensive loading
std::thread::sleep(std::time::Duration::from_millis(50));
"production-config".to_string()
})
}
fn main() {
let mut handles = vec![];
for i in 0..3 {
let handle = thread::spawn(move || {
let config = get_config();
println!("Thread {} got config: {}", i, config);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
// Can also check if initialized
if CONFIG.get().is_some() {
println!("Config was initialized");
}
}use std::sync::OnceLock;
use std::collections::HashMap;
struct Database {
connections: HashMap<String, String>,
}
impl Database {
fn new() -> Self {
println!("Creating database connection pool...");
let mut connections = HashMap::new();
connections.insert("primary".to_string(), "db://localhost:5432".to_string());
connections.insert("replica".to_string(), "db://replica:5432".to_string());
Self { connections }
}
fn get_connection(&self, name: &str) -> Option<&String> {
self.connections.get(name)
}
}
static DB: OnceLock<Database> = OnceLock::new();
fn get_database() -> &'static Database {
DB.get_or_init(Database::new)
}
fn main() {
// First access initializes
let db = get_database();
println!("Primary: {:?}", db.get_connection("primary"));
// Subsequent accesses reuse the same instance
let db2 = get_database();
println!("Replica: {:?}", db2.get_connection("replica"));
// Can also initialize with result
let result: Result<i32, &str> = Ok(42);
static RESULT: OnceLock<Result<i32, &str>> = OnceLock::new();
RESULT.set(result).ok(); // Returns Err if already set
}use std::sync::LazyLock;
use std::time::Instant;
// LazyLock combines OnceLock with initialization function
// The closure runs on first access
static START_TIME: LazyLock<Instant> = LazyLock::new(|| {
println!("Capturing start time...");
Instant::now()
});
static NUMBERS: LazyLock<Vec<i32>> = LazyLock::new(|| {
println!("Generating numbers...");
(1..=100).collect()
});
fn main() {
println!("Before accessing START_TIME");
// First access triggers initialization
println!("Start time: {:?}", *START_TIME);
// Already initialized, no closure runs
println!("Start time again: {:?}", *START_TIME);
// Can access like normal static
println!("Numbers sum: {}", NUMBERS.iter().sum::<i32>());
}use std::cell::OnceCell;
// OnceCell is the single-threaded version
// Use when you don't need thread safety
struct Cache {
value: OnceCell<String>,
}
impl Cache {
fn new() -> Self {
Self { value: OnceCell::new() }
}
fn get_or_compute(&self) -> &String {
self.value.get_or_init(|| {
println!("Computing cached value...");
"expensive-computed-value".to_string()
})
}
}
fn main() {
let cache = Cache::new();
println!("First call:");
println!("Value: {}", cache.get_or_compute());
println!("\nSecond call:");
println!("Value: {}", cache.get_or_compute());
// Check if set
println!("\nIs set? {}", cache.value.get().is_some());
// Try to set (returns Err if already set)
let result = cache.value.set("new value".to_string());
println!("Set result: {:?}", result);
}use std::sync::OnceLock;
use std::fs;
static SETTINGS: OnceLock<String> = OnceLock::new();
fn load_settings() -> Result<&'static String, std::io::Error> {
SETTINGS.get_or_try_init(|| {
println!("Loading settings from file...");
// Simulate file reading
fs::read_to_string("/nonexistent/settings.toml")
.or_else(|_| Ok("default=settings\nvalue=42".to_string()))
})
}
fn main() {
match load_settings() {
Ok(settings) => println!("Settings loaded: {}", settings),
Err(e) => println!("Failed to load settings: {}", e),
}
// Second call returns the cached value
match load_settings() {
Ok(settings) => println!("Cached settings: {}", settings.lines().count()),
Err(_) => unreachable!(),
}
}use std::sync::OnceLock;
use std::collections::HashMap;
pub struct Registry {
items: HashMap<String, String>,
}
impl Registry {
fn new() -> Self {
let mut items = HashMap::new();
items.insert("service_a".to_string(), "http://service-a:8080".to_string());
items.insert("service_b".to_string(), "http://service-b:8080".to_string());
Self { items }
}
pub fn get(&self, key: &str) -> Option<&String> {
self.items.get(key)
}
pub fn all(&self) -> &HashMap<String, String> {
&self.items
}
}
// Global singleton instance
static REGISTRY: OnceLock<Registry> = OnceLock::new();
pub fn registry() -> &'static Registry {
REGISTRY.get_or_init(Registry::new)
}
fn main() {
// Access the singleton
let reg = registry();
println!("Service A: {:?}", reg.get("service_a"));
// Same instance every time
let reg2 = registry();
println!("Same instance? {}", std::ptr::eq(reg, reg2));
}use std::sync::{Once, OnceLock, LazyLock};
static ONCE: Once = Once::new();
static ONCE_LOCK: OnceLock<i32> = OnceLock::new();
static LAZY: LazyLock<i32> = LazyLock::new(|| {
println!("LazyLock initializing...");
42
});
fn main() {
println!("=== Once ===");
// Once only runs code, doesn't return a value
ONCE.call_once(|| {
println!("Once: running initialization");
// Must store result elsewhere if needed
});
println!("Once: second call does nothing");
ONCE.call_once(|| {
println!("This won't print");
});
println!("\n=== OnceLock ===");
// OnceLock stores and returns a value
let value = ONCE_LOCK.get_or_init(|| {
println!("OnceLock: initializing");
100
});
println!("OnceLock value: {}", value);
// get_or_init returns reference to stored value
let value2 = ONCE_LOCK.get_or_init(|| {
println!("This won't print");
200
});
println!("OnceLock value again: {}", value2);
println!("\n=== LazyLock ===");
// LazyLock is initialized on first dereference
println!("LazyLock value: {}", *LAZY);
println!("LazyLock value again: {}", *LAZY);
println!("\n=== Summary ===");
println!("Once: Run code once, no value stored");
println!("OnceLock: Store value, explicit get_or_init");
println!("LazyLock: Store value, implicit initialization");
}use std::sync::OnceLock;
use std::fs::File;
use std::io::Write;
use std::time::SystemTime;
struct Logger {
file: File,
}
impl Logger {
fn new() -> Self {
let file = File::create("app.log").expect("Failed to create log file");
Self { file }
}
fn log(&mut self, message: &str) {
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
writeln!(self.file, "[{}] {}", timestamp, message).unwrap();
}
}
static LOGGER: OnceLock<std::sync::Mutex<Logger>> = OnceLock::new();
fn log(message: &str) {
let logger = LOGGER.get_or_init(|| {
std::sync::Mutex::new(Logger::new())
});
logger.lock().unwrap().log(message);
}
fn main() {
std::thread::scope(|s| {
s.spawn(|| {
log("Message from thread 1");
});
s.spawn(|| {
log("Message from thread 2");
});
s.spawn(|| {
log("Message from thread 3");
});
});
println!("Log file created. Check app.log");
}use std::sync::Once;
use std::sync::atomic::{AtomicBool, Ordering};
static INIT: Once = Once::new();
static IS_INITIALIZED: AtomicBool = AtomicBool::new(false);
fn initialize() {
INIT.call_once(|| {
println!("Running one-time initialization...");
// Do initialization work
std::thread::sleep(std::time::Duration::from_millis(50));
IS_INITIALIZED.store(true, Ordering::SeqCst);
println!("Initialization complete!");
});
}
fn is_ready() -> bool {
IS_INITIALIZED.load(Ordering::SeqCst)
}
fn main() {
println!("Ready? {}", is_ready());
initialize();
println!("Ready? {}", is_ready());
// Calling again does nothing
initialize();
println!("Ready? {}", is_ready());
}// For Rust versions before 1.70, use the once_cell crate
// Add to Cargo.toml:
// [dependencies]
// once_cell = "1.18"
// The once_cell crate provides the same functionality:
// use once_cell::sync::Lazy;
// use once_cell::sync::OnceCell;
// Example with the standard library equivalents (Rust 1.70+):
use std::sync::OnceLock;
fn main() {
// Equivalent to once_cell::sync::OnceCell
static CELL: OnceLock<String> = OnceLock::new();
CELL.set("Hello".to_string()).ok();
println!("Cell value: {:?}", CELL.get());
// For older Rust, the once_cell crate provides:
// once_cell::sync::OnceCell (same as std::sync::OnceLock)
// once_cell::sync::Lazy (same as std::sync::LazyLock)
// once_cell::unsync::OnceCell (same as std::cell::OnceCell)
}| Type | Thread Safe | Use Case |
|------|-------------|----------|
| Once | Yes | Run code exactly once, no value needed |
| OnceLock<T> | Yes | Store value, explicit initialization |
| LazyLock<T> | Yes | Store value, implicit initialization |
| OnceCell<T> | No | Single-threaded lazy storage |
Key Methods:
| Method | Description |
|--------|-------------|
| Once::call_once(&self, f) | Run closure exactly once |
| OnceLock::get(&self) | Get reference if initialized |
| OnceLock::get_or_init(&self, f) | Get or initialize with closure |
| OnceLock::get_or_try_init(&self, f) | Get or initialize with Result |
| OnceLock::set(&self, value) | Set value, returns Err if already set |
| LazyLock::new(f) | Create with initialization closure |
When to Use Each:
| Scenario | Recommended Type |
|----------|-----------------|
| One-time setup without value | Once |
| Lazy static value | OnceLock or LazyLock |
| Singleton pattern | OnceLock |
| Single-threaded cache | OnceCell |
| Fallible initialization | OnceLock with get_or_try_init |
Key Points:
OnceLock for thread-safe one-time initialization (Rust 1.70+)LazyLock for convenient static initialization (Rust 1.80+)OnceCell for single-threaded scenariosonce_cell crate provides the same functionality