Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
118: Once and OnceCell
There's a recurring pattern in almost every production app I've built: the "Global Configuration" problem. You have a set of settings—maybe a database URL, an API key, or a log level—that you load from a file or environment variables when the app starts. You only want to do this expensive I/O once, but you need those values accessible from anywhere in your codebase without passing a config object through every single function signature in your call stack.
The "Trust Me" Approach with static mut
When I first started with Rust, my instinct was to reach for static mut. It seems straightforward: create a global variable, mark it as mutable, and wrap it in an Option. In my head, I knew that I'd initialize it during the main setup and never touch it again. But Rust's borrow checker doesn't trust my intentions; it only sees that any access to a static mut is inherently unsafe because multiple threads could potentially race to read or write it.
static mut CONFIG: Option = None;
fn get_config() -> &'static Config {
unsafe {
// This is a disaster waiting to happen in a multi-threaded app
CONFIG.as_ref().expect("Config not initialized")
}
}
The problem here is that we've just bypassed the entire reason we use Rust. If two threads call an initialization function at the same time, you have a data race. Even if you're "sure" it only happens once, the compiler forces you to wrap every single access in an unsafe block. That’s a lot of noise for something that should be a basic architectural pattern.
Paying the Mutex Tax
To avoid unsafe, the next logical step is usually a Mutex. You wrap your Option in a Mutex, and now the compiler is happy. But now we've traded safety for a performance penalty. Every single time any part of your app needs to check a configuration value, it has to acquire a lock, even though the value never changes after the first five milliseconds of the program's life.
I've seen this in large codebases where a high-frequency loop calls a function that checks a config value. The lock contention becomes a legitimate bottleneck. It feels wrong to pay a synchronization tax on every read for a value that is effectively a constant after startup.
Initialization without the Overhead
This is where OnceLock (the thread-safe version of OnceCell) comes in. Think of it as a write-once, read-many container. It allows you to define a global variable that starts empty and is filled exactly once. The magic is that OnceLock handles the synchronization internally only during the initialization phase.
Once the value is set, subsequent calls to get() are essentially just pointer dereferences. You get the safety of a Mutex during the "write" and the speed of a static reference during the "read".
use std::sync::OnceLock;
static CONFIG: OnceLock<Config> = OnceLock::new();
fn get_config() -> &'static Config {
CONFIG.get_or_init(|| {
// This closure runs exactly once across the entire program
Config::load_from_env()
})
}
If you don't actually need to store a value, but you just need to ensure a specific piece of setup code—like initializing a logging framework or a telemetry exporter—runs exactly once, use std::sync::Once. It doesn't return a value; it just guarantees that the provided closure is executed once, regardless of how many threads attempt to call it.
I generally prefer OnceLock for state and Once for side-effects. It keeps the intent clear: "I need this piece of data" versus "I need this action to happen."
📋 Practical Task
Refactoring the Global Logger Registry
You have been handed a legacy module that manages a global list of active logger names. Currently, it uses a Mutex<Option<Vec<String>>>, which is causing performance degradation in the application's hot path due to lock contention during read-only access.
Your Task: Refactor the following code to use std::sync::OnceLock. Ensure that the get_loggers function no longer returns a MutexGuard, but instead returns a shared reference &'static [String].
use std::sync::Mutex;
struct LoggerRegistry {
loggers: Vec<String>,
}
static REGISTRY: Mutex<Option<LoggerRegistry>> = Mutex::new(None);
fn init_registry(names: Vec<String>) {
let mut lock = REGISTRY.lock().unwrap();
*lock = Some(LoggerRegistry { loggers: names });
}
fn get_loggers() -> std::sync::MutexGuard<'static, Option<LoggerRegistry>> {
REGISTRY.lock().unwrap()
}
fn main() {
init_registry(vec!["Network".to_string(), "Database".to_string()]);
let loggers = get_loggers();
println!("Active loggers: {:?}", loggers.as_ref().unwrap().loggers);
}There are no comments for now.