Fundamentals 8 min read

How to Choose Between Rust’s LazyLock, LazyCell, OnceLock, and OnceCell

Rust 1.80 introduces LazyCell and LazyLock, synchronized lazy-initialization primitives that differ from OnceCell and OnceLock in thread safety and built‑in initialization, and this article explains their characteristics, trade‑offs, and provides concrete code examples to help developers select the appropriate type for their use case.

BirdNest Tech Talk
BirdNest Tech Talk
BirdNest Tech Talk
How to Choose Between Rust’s LazyLock, LazyCell, OnceLock, and OnceCell

Rust 1.80.0 adds two new synchronous primitives, LazyCell and LazyLock, which delay data initialization until the first access. Unlike the earlier OnceCell and OnceLock, the initialization function is embedded in the cell itself. This stabilizes functionality that previously required the external lazy_static and once_cell crates, so developers can now rely on the standard library for lazy initialization.

Differences

LazyLock

: Thread‑safe. The first thread that accesses the static value performs the one‑time initialization, and all threads subsequently see the same value. LazyCell: Not thread‑safe. It does not implement Sync, so it cannot be used in a normal static, but it works inside a thread_local! static where each thread gets its own independent initialization. OnceLock: Thread‑safe. It can be initialized only once; after a lock is acquired it cannot be re‑initialized, making it useful for global mutable state that must not be recreated. OnceCell: Not thread‑safe. The cell can be set a single time and then becomes immutable, typically used for lazily initialized global variables.

In summary, LazyLock and LazyCell provide a built‑in initialization function, whereas OnceLock and OnceCell require manual initialization. Choose the type that matches your thread‑safety requirements and whether you need an automatic initializer.

LazyLock

LazyLock

is thread‑safe, making it suitable for static values that must be shared across threads. In the example below, a spawned thread and the main thread both observe the same elapsed time because the static LAZY_TIME is initialized exactly once by whichever thread accesses it first. This differs from OnceLock::get_or_init(), which requires an explicit call to perform initialization.

use std::sync::LazyLock;
use std::time::Instant;

// The initializer runs only on the first access.
static LAZY_TIME: LazyLock<Instant> = LazyLock::new(Instant::now);

fn main() {
    let start = Instant::now();
    std::thread::scope(|s| {
        s.spawn(|| {
            println!("child thread lazy time is {:?}", LAZY_TIME.duration_since(start));
        });
        println!("main thread lazy time is {:?}", LAZY_TIME.duration_since(start));
    });
}

LazyCell

LazyCell

operates without thread synchronization and therefore does not implement Sync. It can still be used inside a thread_local! static, giving each thread its own separate initialization.

fn main() {
    use std::cell::LazyCell;

    let lazy: LazyCell<i32> = LazyCell::new(|| {
        println!("initializing");
        92
    });
    println!("ready");
    println!("{}", *lazy);
    println!("{}", *lazy);
    // Prints:
    //   ready
    //   initializing
    //   92
    //   92
}

OnceLock

OnceLock

provides a lock that can be initialized only once. It offers methods such as get, get_or_init, and set. The example demonstrates a static OnceLock that is lazily initialized by a child thread; subsequent accesses return the stored value.

#![allow(unused)]
fn main() {
    use std::sync::OnceLock;

    static CELL: OnceLock<usize> = OnceLock::new();

    // `OnceLock` is not yet initialized.
    assert!(CELL.get().is_none());

    // Spawn a thread that attempts initialization.
    std::thread::spawn(|| {
        let value = CELL.get_or_init(|| 12345);
        assert_eq!(value, &12345);
    })
    .join()
    .unwrap();

    // `OnceLock` is now initialized and can be read.
    assert_eq!(CELL.get(), Some(&12345));
}

OnceCell

OnceCell

is a non‑thread‑safe cell that can be set exactly once. Its primary methods are set, get, and get_or_init. The following code shows a OnceCell used to lazily create a String value.

fn main() {
    use std::cell::OnceCell;

    let cell = OnceCell::new();
    assert!(cell.get().is_none());

    let value: &String = cell.get_or_init(|| {
        "Hello, World!".to_string()
    });
    assert_eq!(value, "Hello, World!");
    assert!(cell.get().is_some());
}
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

concurrencyRustlazy-initializationStandard LibrarySync Primitives
BirdNest Tech Talk
Written by

BirdNest Tech Talk

Author of the rpcx microservice framework, original book author, and chair of Baidu's Go CMC committee.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.