How do I work with Once for One-Time Initialization in Rust?

Walkthrough

Once is a synchronization primitive that ensures a piece of initialization code is executed exactly once, even when called from multiple threads. It's useful for lazy initialization of global state, singleton patterns, and ensuring thread-safe one-time setup.

Key concepts:

  • call_once — Executes a closure exactly once, blocking other threads until completion
  • OnceLock — Newer type (Rust 1.70+) for one-time initialization with a value
  • LazyLock — Newest type (Rust 1.80+) for lazy static initialization
  • Thread-safe — Multiple threads can call call_once safely

When to use Once:

  • Initializing global/static state
  • Singleton pattern implementation
  • Lazy initialization of expensive resources
  • Thread-safe one-time setup

When NOT to use Once:

  • Per-thread initialization (use thread_local!)
  • Simple optional values (use Option with Mutex)
  • When you need to re-initialize (design differently)

Code Examples

Basic Once Usage

use std::sync::Once;
use std::thread;
 
static INIT: Once = Once::new();
 
fn expensive_setup() {
    println!("Running expensive setup...");
    thread::sleep(std::time::Duration::from_millis(100));
    println!("Setup complete!");
}
 
fn main() {
    let mut handles = vec![];
    
    for i in 0..5 {
        let handle = thread::spawn(move || {
            println!("Thread {} starting", i);
            INIT.call_once(|| {
                expensive_setup();
            });
            println!("Thread {} proceeding", i);
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

OnceLock for One-Time Value Initialization

use std::sync::OnceLock;
use std::thread;
 
static CONFIG: OnceLock<Vec<String>> = OnceLock::new();
 
fn get_config() -> &'static Vec<String> {
    CONFIG.get_or_init(|| {
        println!("Initializing config...");
        vec![
            String::from("host=localhost"),
            String::from("port=8080"),
            String::from("debug=true"),
        ]
    })
}
 
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();
    }
}

LazyLock for Static Initialization (Rust 1.80+)

use std::sync::LazyLock;
use std::thread;
 
static DATABASE_URL: LazyLock<String> = LazyLock::new(|| {
    println!("Building database URL...");
    String::from("postgres://user:pass@localhost/db")
});
 
static NUMBERS: LazyLock<Vec<i32>> = LazyLock::new(|| {
    println!("Generating numbers...");
    (1..=100).collect()
});
 
fn main() {
    let mut handles = vec![];
    
    for i in 0..3 {
        let handle = thread::spawn(move || {
            println!("Thread {} accessing...", i);
            println!("  URL: {}", *DATABASE_URL);
            println!("  Sum: {}", NUMBERS.iter().sum::<i32>());
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

Singleton Pattern with OnceLock

use std::sync::{OnceLock, Mutex};
use std::thread;
 
struct Database {
    connections: Mutex<u32>,
}
 
impl Database {
    fn new() -> Self {
        println!("Creating database singleton...");
        Self {
            connections: Mutex::new(0),
        }
    }
    
    fn connect(&self) {
        let mut conns = self.connections.lock().unwrap();
        *conns += 1;
        println!("Connection count: {}", *conns);
    }
}
 
static DB: OnceLock<Database> = OnceLock::new();
 
fn get_db() -> &'static Database {
    DB.get_or_init(Database::new)
}
 
fn main() {
    let mut handles = vec![];
    
    for i in 0..3 {
        let handle = thread::spawn(move || {
            let db = get_db();
            println!("Thread {} connecting", i);
            db.connect();
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

Once vs OnceLock Comparison

use std::sync::{Once, OnceLock};
use std::thread;
 
static ONCE: Once = Once::new();
static ONCE_LOCK: OnceLock<String> = OnceLock::new();
 
fn main() {
    // Once: for side effects, no return value
    ONCE.call_once(|| {
        println!("One-time setup with Once");
    });
    
    // OnceLock: for initializing a value
    let value = ONCE_LOCK.get_or_init(|| {
        println!("One-time init with OnceLock");
        String::from("Initialized value")
    });
    
    println!("Value: {}", value);
    
    // Subsequent calls don't re-initialize
    let value2 = ONCE_LOCK.get_or_init(|| {
        panic!("This won't run!");
    });
    
    println!("Same value: {}", value2);
}

OnceLock with get_or_try_init

use std::sync::OnceLock;
use std::fs;
 
static FILE_CONTENTS: OnceLock<String> = OnceLock::new();
 
fn get_file_contents() -> Result<&'static str, std::io::Error> {
    FILE_CONTENTS.get_or_try_init(|| {
        fs::read_to_string("config.txt")
            .or_else(|_| Ok(String::from("default config")))
    }).map(|s| s.as_str())
}
 
fn main() {
    match get_file_contents() {
        Ok(contents) => println!("Contents: {}", contents),
        Err(e) => println!("Error: {}", e),
    }
}

Once for Logger Initialization

use std::sync::Once;
use std::sync::Mutex;
use std::thread;
use std::io::Write;
 
static LOGGER_INIT: Once = Once::new();
static LOG_MUTEX: Mutex<()> = Mutex::new(());
 
fn log(message: &str) {
    LOGGER_INIT.call_once(|| {
        // One-time logger setup
        eprintln!("[LOGGER] Initialized");
    });
    
    let _guard = LOG_MUTEX.lock().unwrap();
    eprintln!("[LOG] {}", message);
}
 
fn main() {
    let mut handles = vec![];
    
    for i in 0..5 {
        let handle = thread::spawn(move || {
            log(&format!("Message from thread {}", i));
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

Thread-Safe Lazy Computation

use std::sync::OnceLock;
use std::thread;
 
struct ExpensiveComputation {
    data: Vec<u64>,
}
 
impl ExpensiveComputation {
    fn compute() -> Self {
        println!("Computing expensive data...");
        thread::sleep(std::time::Duration::from_millis(200));
        
        let data: Vec<u64> = (0..1000).map(|x| x * x).collect();
        Self { data }
    }
}
 
static COMPUTATION: OnceLock<ExpensiveComputation> = OnceLock::new();
 
fn get_computation() -> &'static ExpensiveComputation {
    COMPUTATION.get_or_init(ExpensiveComputation::compute)
}
 
fn main() {
    let mut handles = vec![];
    
    for i in 0..3 {
        let handle = thread::spawn(move || {
            let comp = get_computation();
            let sum: u64 = comp.data.iter().sum();
            println!("Thread {} sum: {}", i, sum);
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

Once with State Tracking

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Once;
use std::thread;
 
static INIT: Once = Once::new();
static INITIALIZED: AtomicBool = AtomicBool::new(false);
 
fn ensure_initialized() {
    INIT.call_once(|| {
        println!("Initializing...");
        thread::sleep(std::time::Duration::from_millis(100));
        INITIALIZED.store(true, Ordering::SeqCst);
        println!("Done!");
    });
}
 
fn is_initialized() -> bool {
    INITIALIZED.load(Ordering::SeqCst)
}
 
fn main() {
    println!("Before: initialized = {}", is_initialized());
    
    ensure_initialized();
    
    println!("After: initialized = {}", is_initialized());
    
    // Calling again does nothing
    ensure_initialized();
    println!("Final: initialized = {}", is_initialized());
}

OnceLock for Configuration

use std::sync::OnceLock;
use std::collections::HashMap;
 
#[derive(Debug)]
struct Config {
    settings: HashMap<String, String>,
}
 
impl Config {
    fn load() -> Self {
        println!("Loading configuration...");
        let mut settings = HashMap::new();
        settings.insert("host".to_string(), "localhost".to_string());
        settings.insert("port".to_string(), "8080".to_string());
        settings.insert("debug".to_string(), "true".to_string());
        Self { settings }
    }
    
    fn get(&self, key: &str) -> Option<&String> {
        self.settings.get(key)
    }
}
 
static CONFIG: OnceLock<Config> = OnceLock::new();
 
fn config() -> &'static Config {
    CONFIG.get_or_init(Config::load)
}
 
fn main() {
    println!("Host: {:?}", config().get("host"));
    println!("Port: {:?}", config().get("port"));
    println!("Debug: {:?}", config().get("debug"));
}

Multiple Once Primitives

use std::sync::{Once, OnceLock};
use std::thread;
 
static DB_INIT: Once = Once::new();
static CACHE_INIT: Once = Once::new();
static LOGGER: OnceLock<String> = OnceLock::new();
 
fn init_database() {
    DB_INIT.call_once(|| {
        println!("Database initialized");
    });
}
 
fn init_cache() {
    CACHE_INIT.call_once(|| {
        println!("Cache initialized");
    });
}
 
fn get_logger() -> &'static str {
    LOGGER.get_or_init(|| {
        println!("Logger initialized");
        String::from("app_logger")
    })
}
 
fn main() {
    init_database();
    init_cache();
    println!("Logger: {}", get_logger());
    
    // Subsequent calls are no-ops
    init_database();
    init_cache();
    println!("Logger: {}", get_logger());
}

OnceLock with Interior Mutability

use std::sync::{OnceLock, Mutex};
use std::thread;
 
struct Counter {
    value: Mutex<i32>,
}
 
impl Counter {
    fn new() -> Self {
        println!("Creating counter...");
        Self {
            value: Mutex::new(0),
        }
    }
    
    fn increment(&self) -> i32 {
        let mut v = self.value.lock().unwrap();
        *v += 1;
        *v
    }
}
 
static COUNTER: OnceLock<Counter> = OnceLock::new();
 
fn main() {
    let mut handles = vec![];
    
    for i in 0..5 {
        let handle = thread::spawn(move || {
            let counter = COUNTER.get_or_init(Counter::new);
            let value = counter.increment();
            println!("Thread {} incremented to {}", i, value);
        });
        handles.push(handle);
    }
    
    for handle in handles {
        handle.join().unwrap();
    }
}

lazy_static vs OnceLock Comparison

// Note: lazy_static requires the crate, OnceLock is in std
 
use std::sync::OnceLock;
 
// With OnceLock (standard library):
static LAZY_VALUE: OnceLock<Vec<i32>> = OnceLock::new();
 
fn get_lazy_value() -> &'static Vec<i32> {
    LAZY_VALUE.get_or_init(|| {
        println!("Computing lazy value...");
        (1..=10).collect()
    })
}
 
fn main() {
    println!("Before access");
    println!("Value: {:?}", get_lazy_value());
    println!("After first access");
    println!("Value: {:?}", get_lazy_value()); // No re-computation
}
 
// With lazy_static crate (commented):
// #[macro_use]
// extern crate lazy_static;
// 
// lazy_static! {
//     static ref LAZY_VALUE: Vec<i32> = {
//         println!("Computing...");
//         (1..=10).collect()
//     };
// }

Once for Plugin System

use std::sync::{Once, Mutex};
use std::collections::HashMap;
 
type PluginFn = fn() -> String;
 
static PLUGIN_INIT: Once = Once::new();
static PLUGINS: Mutex<HashMap<String, PluginFn>> = Mutex::new(HashMap::new());
 
fn register_plugins() {
    PLUGIN_INIT.call_once(|| {
        let mut plugins = PLUGINS.lock().unwrap();
        plugins.insert("greet".to_string(), greet_plugin);
        plugins.insert("farewell".to_string(), farewell_plugin);
        println!("Plugins registered");
    });
}
 
fn greet_plugin() -> String {
    String::from("Hello, World!")
}
 
fn farewell_plugin() -> String {
    String::from("Goodbye, World!")
}
 
fn run_plugin(name: &str) -> Option<String> {
    register_plugins();
    let plugins = PLUGINS.lock().unwrap();
    plugins.get(name).map(|f| f())
}
 
fn main() {
    println!("{:?}", run_plugin("greet"));
    println!("{:?}", run_plugin("farewell"));
    println!("{:?}", run_plugin("unknown"));
}

OnceLock with Complex Initialization

use std::sync::OnceLock;
use std::thread;
use std::time::Duration;
 
struct Service {
    name: String,
    endpoint: String,
    timeout: Duration,
}
 
impl Service {
    fn new(name: &str, endpoint: &str, timeout_ms: u64) -> Self {
        println!("Creating service: {}", name);
        Self {
            name: name.to_string(),
            endpoint: endpoint.to_string(),
            timeout: Duration::from_millis(timeout_ms),
        }
    }
}
 
static API_SERVICE: OnceLock<Service> = OnceLock::new();
static DB_SERVICE: OnceLock<Service> = OnceLock::new();
 
fn get_api_service() -> &'static Service {
    API_SERVICE.get_or_init(|| Service::new("api", "https://api.example.com", 5000))
}
 
fn get_db_service() -> &'static Service {
    DB_SERVICE.get_or_init(|| Service::new("db", "postgres://localhost/db", 10000))
}
 
fn main() {
    println!("API: {}", get_api_service().endpoint);
    println!("DB: {}", get_db_service().endpoint);
}

Summary

Once Types Comparison:

Type Rust Version Purpose Returns Value
Once Stable One-time execution (side effects) No
OnceLock<T> 1.70+ One-time value initialization Yes
LazyLock<T> 1.80+ Lazy static initialization Yes

Once Methods:

Method Description
new() Create new Once instance
call_once(f) Execute f exactly once
is_completed() Check if initialization completed

OnceLock Methods:

Method Description
new() Create new OnceLock instance
get() Get reference if initialized
get_or_init(f) Initialize if needed, return reference
get_or_try_init(f) Initialize with fallible closure
set(value) Set value if not already set
into_inner(self) Extract inner value

Common Patterns:

Pattern Type Use Case
Singleton OnceLock<T> Single global instance
Lazy config OnceLock<Config> Load config on first access
Logger init Once One-time logger setup
Plugin registry Once + Mutex<HashMap> One-time registration
Service locator OnceLock<Service> Lazy service creation

Key Points:

  • Once for side effects, OnceLock<T> for values
  • Thread-safe by default
  • call_once blocks until completion
  • OnceLock::get_or_init returns &T
  • LazyLock is the modern replacement for lazy_static!
  • Use for global/static initialization
  • get_or_try_init for fallible initialization
  • Cannot re-initialize once completed
  • Combine with Mutex for mutable global state