ExamplesBy LevelBy TopicLearning Paths
456 Fundamental

456: `OnceLock` and `OnceCell` — Once-Initialized Values

Functional Programming

Tutorial

The Problem

Some values are expensive to initialize (parse config, compile regex, connect to DB) and should only be initialized once. Race conditions arise if multiple threads try to initialize simultaneously. OnceLock<T> provides thread-safe once-initialization: get_or_init(|| expensive_computation()) guarantees the closure runs exactly once, even under concurrent access. OnceCell<T> is the single-threaded version. Both are simpler and more ergonomic than the lazy_static! macro they largely replace.

OnceLock is used for global singletons, global configuration, compiled regex caches, connection pools initialized once, and any value needing lazy evaluation with thread-safe initialization guarantee.

🎯 Learning Outcomes

  • • Understand the difference between OnceLock<T> (thread-safe) and OnceCell<T> (single-thread)
  • • Learn how get_or_init guarantees exactly-once initialization under concurrent access
  • • See how static CONFIG: OnceLock<HashMap<...>> creates a global lazy singleton
  • • Understand that OnceLock is the modern replacement for lazy_static! for most use cases
  • • Learn when OnceCell suffices (no concurrent access to the cell itself)
  • Code Example

    #![allow(clippy::all)]
    // 456. OnceLock and OnceCell for lazy init
    use std::cell::OnceCell;
    use std::collections::HashMap;
    use std::sync::OnceLock;
    
    static CONFIG: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
    static GREETING: OnceLock<String> = OnceLock::new();
    
    fn config() -> &'static HashMap<&'static str, &'static str> {
        CONFIG.get_or_init(|| {
            println!("init config");
            [("host", "localhost"), ("port", "8080")]
                .iter()
                .cloned()
                .collect()
        })
    }
    
    #[cfg(test)]
    mod tests {
        use std::sync::atomic::{AtomicU32, Ordering};
        use std::sync::OnceLock;
        #[test]
        fn test_once_only() {
            let lock: OnceLock<u32> = OnceLock::new();
            let n = AtomicU32::new(0);
            lock.get_or_init(|| {
                n.fetch_add(1, Ordering::SeqCst);
                42
            });
            lock.get_or_init(|| {
                n.fetch_add(1, Ordering::SeqCst);
                99
            });
            assert_eq!(*lock.get().unwrap(), 42);
            assert_eq!(n.load(Ordering::SeqCst), 1);
        }
    }

    Key Differences

  • Language vs. library: OCaml's lazy is a language keyword; Rust's OnceLock is a standard library type.
  • Global statics: Rust static OnceLock enables lazy global initialization; OCaml let global_val = lazy (...) achieves the same at module level.
  • Panic safety: Rust's OnceLock handles panics in get_or_init by allowing retry; OCaml's Lazy.force re-raises the exception on retry.
  • **once_cell crate**: Before stabilization in std, the once_cell crate provided Lazy<T> which initializes on first deref — slightly more ergonomic than OnceLock.
  • OCaml Approach

    OCaml's Lazy.t is the standard lazy value: let config = lazy (make_config ()) and Lazy.force config for access. Lazy.t is thread-safe in OCaml 5.x with the standard library's guarantee of single evaluation. In OCaml 4.x, Lazy.t uses a mutex internally for thread safety. The Lazy.from_val function creates an already-evaluated lazy value.

    Full Source

    #![allow(clippy::all)]
    // 456. OnceLock and OnceCell for lazy init
    use std::cell::OnceCell;
    use std::collections::HashMap;
    use std::sync::OnceLock;
    
    static CONFIG: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
    static GREETING: OnceLock<String> = OnceLock::new();
    
    fn config() -> &'static HashMap<&'static str, &'static str> {
        CONFIG.get_or_init(|| {
            println!("init config");
            [("host", "localhost"), ("port", "8080")]
                .iter()
                .cloned()
                .collect()
        })
    }
    
    #[cfg(test)]
    mod tests {
        use std::sync::atomic::{AtomicU32, Ordering};
        use std::sync::OnceLock;
        #[test]
        fn test_once_only() {
            let lock: OnceLock<u32> = OnceLock::new();
            let n = AtomicU32::new(0);
            lock.get_or_init(|| {
                n.fetch_add(1, Ordering::SeqCst);
                42
            });
            lock.get_or_init(|| {
                n.fetch_add(1, Ordering::SeqCst);
                99
            });
            assert_eq!(*lock.get().unwrap(), 42);
            assert_eq!(n.load(Ordering::SeqCst), 1);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use std::sync::atomic::{AtomicU32, Ordering};
        use std::sync::OnceLock;
        #[test]
        fn test_once_only() {
            let lock: OnceLock<u32> = OnceLock::new();
            let n = AtomicU32::new(0);
            lock.get_or_init(|| {
                n.fetch_add(1, Ordering::SeqCst);
                42
            });
            lock.get_or_init(|| {
                n.fetch_add(1, Ordering::SeqCst);
                99
            });
            assert_eq!(*lock.get().unwrap(), 42);
            assert_eq!(n.load(Ordering::SeqCst), 1);
        }
    }

    Exercises

  • Global logger: Create static LOGGER: OnceLock<Logger> where Logger reads its configuration from environment variables. Verify that the environment is read only once and subsequent calls use the cached configuration.
  • Regex cache: Implement fn is_valid_ipv4(s: &str) -> bool using static RE: OnceLock<Regex>. Verify with 10 concurrent threads calling the function that the regex is compiled exactly once.
  • OnceCell injection: Use OnceCell<Box<dyn Database>> in a struct AppState to enable injecting different database implementations in tests vs. production, initializing once via set from the setup code.
  • Open Source Repos