Rust provides multiple ways to initialize data structures and global state, each suited for different constraints around compile-time vs. runtime, sync vs. async, and single-threaded vs. multi-threaded execution. Choosing the right technique depends on whether the value is known at compile-time, requires lazy evaluation, or needs thread-local or async-aware initialization.
Compile-Time Constants
When a value is known at compile-time and does not require modification, const provides an efficient, zero-cost way to define immutable values. These constants are embedded directly into the binary and do not require allocation.
const MAX_CONNECTIONS: usize = 1024;Unlike static, const items do not have a fixed memory address and are evaluated wherever they are used.
Global Static Variables
If a value must exist at a single memory location for the duration of the program, static ensures global accessibility. Static variables must have a Sync type unless wrapped in synchronization primitives.
static GREETING: &str = "Hello, World!";Unlike const, a static variable has a fixed memory address and is mutable only with unsafe code.
static mut COUNTER: u32 = 0;
fn increment() {
unsafe { COUNTER += 1; }
}Runtime Lazy Initialization
Some values require lazy evaluation to avoid unnecessary computations or ensure correct ordering of initialization. Rust offers multiple ways to achieve this safely.
std::sync::Once
If a value must be initialized exactly once across multiple threads, std::sync::Once ensures that initialization occurs only once, even under concurrent access.
use std::sync::{Once, ONCE_INIT};
static INIT: Once = Once::new();
static mut CONFIG: Option<String> = None;
fn initialize() {
INIT.call_once(|| {
unsafe { CONFIG = Some("Loaded Config".to_string()); }
});
}While Once guarantees single execution, accessing the initialized value still requires unsafe code.
A safer alternative to Once is OnceCell, which allows storing and retrieving an initialized value without unsafe code.
use std::sync::OnceCell;
static CONFIG: OnceCell<String> = OnceCell::new();
fn get_config() -> &'static str {
CONFIG.get_or_init(|| "Default Config".to_string())
}Unlike Once, OnceCell allows retrieving the initialized value safely without needing manual synchronization.
LazyLock
For scenarios where lazy initialization is needed but mutation is not, LazyLock is an optimized alternative that avoids unnecessary locking when accessing the value.
use std::sync::LazyLock;
static CONFIG: LazyLock<String> = LazyLock::new(|| "Lazy Loaded Config".to_string());Unlike OnceCell, LazyLock requires no explicit initialization call, making it useful for read-heavy workloads.
Thread-Local Storage
If a variable must be unique to each thread, thread_local! ensures that each thread gets its own instance of a value.
use std::cell::RefCell;
thread_local! {
static COUNTER: RefCell<u32> = RefCell::new(0);
}
fn increment() {
COUNTER.with(|counter| *counter.borrow_mut() += 1);
}Since each thread has its own COUNTER, synchronization is not required, but the value is only accessible within the same thread.
Async Initialization
For asynchronous lazy initialization, tokio::sync::OnceCell provides a non-blocking way to initialize data only when first needed.
use tokio::sync::OnceCell;
static CONFIG: OnceCell<String> = OnceCell::const_new();
async fn get_config() -> &'static String {
CONFIG.get_or_init(|| async {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
"Async Config".to_string()
}).await
}Unlike std::sync::OnceCell, the async variant avoids blocking and allows lazy evaluation within an async runtime like Tokio.
Task-Local Storage in Async Contexts
Since thread_local! does not work in async runtimes (which use tasks, not OS threads), tokio::task_local! ensures that each async task gets its own independent storage.
use tokio::task_local;
task_local! {
static REQUEST_ID: u32;
}
async fn process_request() {
REQUEST_ID.scope(42, async {
println!("Task-local ID: {}", REQUEST_ID.get());
}).await;
}Unlike thread_local!, this approach ensures the value is propagated correctly across .await points.