Rust Once 、OnceLock、LazyLock 一次性初始化详解

Rust Once 、OnceLock、LazyLock 一次性初始化详解

    • [1、 引言](#1、 引言)
    • [2、 为什么需要一次性初始化](#2、 为什么需要一次性初始化)
      • [2.1、 全局可变状态的困境](#2.1、 全局可变状态的困境)
      • [2.2、 常见替代方案的缺陷](#2.2、 常见替代方案的缺陷)
    • [3、 Once:只执行一次的同步原语](#3、 Once:只执行一次的同步原语)
      • [3.1、 基本概念](#3.1、 基本概念)
      • [3.2、 基础用法](#3.2、 基础用法)
      • [3.3 、与全局数据配合](#3.3 、与全局数据配合)
      • [3.4 、适用场景](#3.4 、适用场景)
    • 4、OnceLock<T>:可存储数据的一次性容器
      • [4.1、 基本概念](#4.1、 基本概念)
      • [4.2、 基础用法](#4.2、 基础用法)
      • [4.3 、线程安全保证](#4.3 、线程安全保证)
      • [4.4 、常用方法](#4.4 、常用方法)
      • [4.5、 适用场景](#4.5、 适用场景)
    • [5、 LazyLock<T>:更简洁的懒加载](#5、 LazyLock:更简洁的懒加载)
      • [5.1、 基本概念](#5.1、 基本概念)
      • [5.2 、基础用法](#5.2 、基础用法)
      • [5.3、 与 OnceLock 的对比](#5.3、 与 OnceLock 的对比)
      • [5.4、 适用场景](#5.4、 适用场景)
    • [6、 三者对比与选型](#6、 三者对比与选型)
    • [7、 实战:全局配置管理器](#7、 实战:全局配置管理器)
    • 8、注意事项与常见陷阱
      • [8.1、 初始化闭包中的递归调用](#8.1、 初始化闭包中的递归调用)
      • [8.2 、不要用 OnceLock 存储非 Send 类型](#8.2 、不要用 OnceLock 存储非 Send 类型)
      • [8.3、 初始化开销](#8.3、 初始化开销)
    • [9、 总结](#9、 总结)
  • 二、代码示例

1、 引言

在 Rust 中,很多场景需要「只初始化一次」的全局或共享数据:全局配置、单例连接池、懒加载的静态资源等。如果直接用 static mut 或裸指针,会引入 unsafe 和数据竞争风险;如果每次访问都重新计算,又会浪费性能。

Rust 标准库为此提供了一套安全、高效的一次性初始化工具:OnceOnceLock<T>LazyLock<T>。本文将从原理到实战,逐一讲解它们的用法、区别与适用场景。

2、 为什么需要一次性初始化

2.1、 全局可变状态的困境

Rust 的 static 变量要求类型实现 Sync,且默认不可变。要存放运行时才能确定的值(如读取配置文件、建立数据库连接),直接写会遇到困难:

rust 复制代码
// 编译失败:static 变量要求编译期常量
static CONFIG: HashMap<String, String> = HashMap::new();

2.2、 常见替代方案的缺陷

方案 问题
static mut 需要 unsafe,易引发数据竞争
lazy_static! 第三方依赖,且宏写法不够直观
std::sync::Once 只解决「执行一次」,不直接保存数据
OnceLock<T> / LazyLock<T> 标准库原生支持,安全且简洁

3、 Once:只执行一次的同步原语

3.1、 基本概念

std::sync::Once 是最底层的一次性初始化原语。它保证传入的闭包在整个程序生命周期内只执行一次,即使多个线程同时调用。

3.2、 基础用法

rust 复制代码
use std::sync::Once;

static INIT: Once = Once::new();

fn init() {
    // 只执行一次的初始化逻辑
    println!("初始化全局资源");
}

fn main() {
    // 多个线程同时调用也只会执行一次
    INIT.call_once(|| {
        init();
    });
}

3.3 、与全局数据配合

Once 本身不保存数据,通常与 static mutUnsafeCell 配合使用,由 Once 保证初始化只发生一次:

rust 复制代码
use std::sync::Once;
use std::sync::Mutex;

static mut CONNECTION: Option<Mutex<String>> = None;
static INIT: Once = Once::new();

fn get_connection() -> &'static Mutex<String> {
    INIT.call_once(|| {
        unsafe {
            CONNECTION = Some(Mutex::new(String::from("数据库连接")));
        }
    });
    unsafe { CONNECTION.as_ref().unwrap() }
}

注意:上面的写法仍然涉及 unsafe。在绝大多数场景下,我们更推荐使用 OnceLock<T> 来避免手写 unsafe

3.4 、适用场景

  • 只需要「执行一次」的副作用,如注册全局钩子、初始化日志系统。
  • 作为更高级封装(如 OnceLock)的内部实现基础。

4、OnceLock<T>:可存储数据的一次性容器

4.1、 基本概念

std::sync::OnceLock<T>Once 的基础上增加了数据存储能力 。它内部使用 Once 保证初始化只执行一次,并通过 UnsafeCell 安全地保存数据。

4.2、 基础用法

rust 复制代码
use std::sync::OnceLock;

static CONFIG: OnceLock<String> = OnceLock::new();

fn get_config() -> &'static str {
    CONFIG.get_or_init(|| {
        // 首次调用时执行,后续直接返回缓存值
        String::from("server=127.0.0.1;port=8080")
    })
}

fn main() {
    println!("配置: {}", get_config());
    println!("再次获取: {}", get_config()); // 不会重复初始化
}

4.3 、线程安全保证

OnceLock<T> 实现了 Sync(当 T: Send + Sync),多个线程可以安全地并发访问:

rust 复制代码
use std::sync::OnceLock;
use std::thread;

static COUNTER: OnceLock<u64> = OnceLock::new();

fn main() {
    let handles: Vec<_> = (0..10)
        .map(|i| {
            thread::spawn(move || {
                let value = COUNTER.get_or_init(|| i * 100);
                println!("线程 {} 获取到: {}", i, value);
            })
        })
        .collect();

    for handle in handles {
        handle.join().unwrap();
    }
}

4.4 、常用方法

方法 说明
get() 获取已初始化的值,未初始化返回 None
get_or_init(f) 获取值,未初始化则执行闭包并缓存
get_or_try_init(f) 初始化可能失败的版本,返回 Result
set(value) 手动设置值,已初始化则返回 Err
into_inner() 取出内部值(消费 OnceLock

4.5、 适用场景

  • 全局单例配置、连接池、缓存。
  • 需要「首次访问时懒加载」的静态数据。
  • 希望避免 unsafe 的线程安全初始化。

5、 LazyLock<T>:更简洁的懒加载

5.1、 基本概念

std::sync::LazyLock<T>OnceLock<T> 的语法糖封装,在 Rust 1.80 版本稳定。它允许在定义静态变量时直接传入初始化闭包,访问时自动完成懒加载。

5.2 、基础用法

rust 复制代码
use std::sync::LazyLock;

static CONFIG: LazyLock<String> = LazyLock::new(|| {
    // 首次访问时执行
    String::from("server=127.0.0.1;port=8080")
});

fn main() {
    // 首次访问触发初始化
    println!("配置: {}", *CONFIG);
    // 后续访问直接复用
    println!("再次访问: {}", *CONFIG);
}

5.3、 与 OnceLock 的对比

rust 复制代码
use std::sync::{LazyLock, OnceLock};

// OnceLock 写法:需要手动调用 get_or_init
static A: OnceLock<Vec<u32>> = OnceLock::new();
fn get_a() -> &'static Vec<u32> {
    A.get_or_init(|| vec![1, 2, 3])
}

// LazyLock 写法:定义时直接指定初始化逻辑
static B: LazyLock<Vec<u32>> = LazyLock::new(|| vec![1, 2, 3]);

fn main() {
    println!("A: {:?}", get_a());
    println!("B: {:?}", *B);
}

5.4、 适用场景

  • 静态变量的懒加载初始化,代码更简洁。
  • 初始化逻辑在定义时就能确定,无需运行时动态决定。

6、 三者对比与选型

特性 Once OnceLock<T> LazyLock<T>
存储数据
需要 unsafe 配合使用时需要 不需要 不需要
初始化时机 手动调用 call_once 首次 get_or_init 首次解引用
写法简洁度
稳定版本 1.0 1.70 1.80
适用场景 纯副作用初始化 需要懒加载的全局数据 定义时即可确定初始化逻辑

选型建议

  • 只需要「执行一次」的副作用 → 用 Once
  • 需要保存数据、首次访问时初始化 → 用 OnceLock<T>
  • 静态变量初始化逻辑简单明确 → 用 LazyLock<T>,代码最简洁。

7、 实战:全局配置管理器

下面是一个综合示例,演示如何用 LazyLock 实现一个线程安全的全局配置管理器:

rust 复制代码
use std::collections::HashMap;
use std::sync::LazyLock;

static CONFIG: LazyLock<HashMap<String, String>> = LazyLock::new(|| {
    // 模拟从配置文件读取
    let mut map = HashMap::new();
    map.insert("host".to_string(), "127.0.0.1".to_string());
    map.insert("port".to_string(), "8080".to_string());
    map.insert("timeout".to_string(), "30".to_string());
    map
});

fn get_config(key: &str) -> Option<&'static str> {
    CONFIG.get(key).map(|s| s.as_str())
}

fn main() {
    println!("host = {}", get_config("host").unwrap());
    println!("port = {}", get_config("port").unwrap());
    println!("timeout = {}", get_config("timeout").unwrap());
}

8、注意事项与常见陷阱

8.1、 初始化闭包中的递归调用

get_or_init 的闭包中再次访问同一个 OnceLock 会导致死锁:

rust 复制代码
use std::sync::OnceLock;

static DATA: OnceLock<u32> = OnceLock::new();

fn main() {
    // 危险:闭包内再次调用 get_or_init 会死锁
    let _ = DATA.get_or_init(|| {
        *DATA.get_or_init(|| 42) + 1
    });
}

8.2 、不要用 OnceLock 存储非 Send 类型

OnceLock<T> 要求 T: Send + Sync 才能跨线程共享。如果只需要单线程使用,可以考虑 std::cell::OnceCell<T>

8.3、 初始化开销

OnceLockLazyLock 的首次访问有少量原子操作开销,但后续访问几乎零成本。对于高频访问的全局数据,这是非常划算的。

9、 总结

  • Once 是最底层的一次性执行原语,适合纯副作用初始化。
  • OnceLock<T>Once 基础上增加了数据存储,是线程安全懒加载的首选。
  • LazyLock<T>OnceLock<T> 的简洁封装,适合静态变量初始化。
  • 三者都基于标准库实现,无需第三方依赖,安全且高效。

在实际项目中,优先考虑 LazyLock<T>OnceLock<T>,尽量避免手写 unsafe 的初始化逻辑。

二、代码示例

rust 复制代码
use std::sync::{Once, OnceLock, LazyLock};
use std::sync::atomic::{AtomicU32, Ordering};
use std::thread;

// ====================== Once  ======================
static G_ONCE: Once = Once::new();
static G_ONCE_VALUE: AtomicU32 = AtomicU32::new(0);

fn demo_once() {
    let mut handles = Vec::new();
    for tid in 0..4 {
        let h = thread::spawn(move || {
            println!("[{}] 尝试执行 Once 初始化", tid);
            G_ONCE.call_once(|| {
                println!("==== Once 初始化逻辑(仅运行1次)====");
                G_ONCE_VALUE.store(100, Ordering::Release);
            });
            let val = G_ONCE_VALUE.load(Ordering::Acquire);
            println!("[{}] Once读取值 = {}", tid, val);
        });
        handles.push(h);
    }
    for h in handles {
        h.join().unwrap();
    }
}

// ====================== OnceLock<T> ======================
static G_ONCE_LOCK: OnceLock<String> = OnceLock::new();

fn demo_once_lock() {
    let mut handles = Vec::new();
    for tid in 0..4 {
        let h = thread::spawn(move || {
            println!("[{}] OnceLock 请求获取/初始化", tid);
            let val = G_ONCE_LOCK.get_or_init(|| {
                println!("==== OnceLock 初始化闭包(仅1次)====");
                format!("once_lock_data_{}", 999)
            });
            println!("[{}] OnceLock value = {}", tid, val);
        });
        handles.push(h);
    }
    for h in handles {
        h.join().unwrap();
    }

    println!("OnceLock 是否初始化完成: {}", G_ONCE_LOCK.get().is_some());
    if let Some(v) = G_ONCE_LOCK.get() {
        println!("get() 读取: {}", v);
    }
}

// ====================== LazyLock<T> ======================
static G_LAZY: LazyLock<Vec<u32>> = LazyLock::new(|| {
    println!("==== LazyLock 自动初始化(第一次解引用时执行)====");
    vec![1, 2, 3, 4, 5]
});

fn demo_lazy_lock() {
    let mut handles = Vec::new();
    for tid in 0..4 {
        let h = thread::spawn(move || {
            println!("[{}] LazyLock 访问", tid);
            let data = &*G_LAZY;
            println!("[{}] LazyLock data = {:?}", tid, data);
        });
        handles.push(h);
    }
    for h in handles {
        h.join().unwrap();
    }
}

fn main() {
    println!("========= Demo Once =========");
    demo_once();

    println!("\n========= Demo OnceLock =========");
    demo_once_lock();

    println!("\n========= Demo LazyLock =========");
    demo_lazy_lock();
}
rust 复制代码
PS E:\leaning\slint\test> cargo run
========= Demo Once =========
[0] 尝试执行 Once 初始化
==== Once 初始化逻辑(仅运行1次)====
[0] Once读取值 = 100
[1] 尝试执行 Once 初始化
[1] Once读取值 = 100
[3] 尝试执行 Once 初始化
[3] Once读取值 = 100
[2] 尝试执行 Once 初始化
[2] Once读取值 = 100

========= Demo OnceLock =========
[0] OnceLock 请求获取/初始化
==== OnceLock 初始化闭包(仅1次)====
[0] OnceLock value = once_lock_data_999
[2] OnceLock 请求获取/初始化
[2] OnceLock value = once_lock_data_999
[1] OnceLock 请求获取/初始化
[1] OnceLock value = once_lock_data_999
[3] OnceLock 请求获取/初始化
[3] OnceLock value = once_lock_data_999
OnceLock 是否初始化完成: true
get() 读取: once_lock_data_999

========= Demo LazyLock =========
[0] LazyLock 访问
==== LazyLock 自动初始化(第一次解引用时执行)====
[0] LazyLock data = [1, 2, 3, 4, 5]
[2] LazyLock 访问
[2] LazyLock data = [1, 2, 3, 4, 5]
[3] LazyLock 访问
[3] LazyLock data = [1, 2, 3, 4, 5]
[1] LazyLock 访问
[1] LazyLock data = [1, 2, 3, 4, 5]
PS E:\leaning\slint\test> 
相关推荐
hqyjzsb1 小时前
规划工商管理大学成长:搭建四层能力体系,重视高阶的 AI 能力建设
开发语言·人工智能·python·microsoft·职场和发展·数据挖掘·业界资讯
我不会起名字3221 小时前
一天一道算法题(35):电话号码的字母组合
java·数据结构·后端·python·leetcode·go·回溯
qq_452396232 小时前
第八篇:《智能指针与内部可变性:Rust 的进阶内存管理》
rust
for_ever_love__2 小时前
爬虫项目: 获取高分电影的数据总结
开发语言·python·学习
weixin_307779132 小时前
C++代码实现MATLAB中的ode23t函数功能
开发语言·c++·算法·matlab
星栈2 小时前
ADK-Rust 是什么?Rust 生态新一代 AI Agent 开发套件
后端·agent
君顾12 小时前
24小时自助健身房系统开发实战与完整指南
java·开发语言·健身房
盖伦发发2 小时前
业务开发常用设计模式:状态、策略、工厂、适配器、模板方法
后端·软件工程
syagain_zsx3 小时前
算法基础篇 · 02 高精度(C++ 题解)
开发语言·c++·学习·高精度