Rust

2025年09月16日更新 8 人订阅
原价: ¥ 6 限时优惠
专栏简介 Rust编程语言之错误处理 Rust 语言之 flod Rust编程语言之Cargo、Crates.io详解 Rust编程语言之枚举与模式匹配 Rust语言 - 接口设计的建议之受约束(Constrained) Rust编程语言之无畏并发 Rust语言 - 接口设计的建议之灵活(flexible) Rust语言 - 接口设计的建议之显而易见(Obvious) Rust语言 - 接口设计的建议之不意外(unsurprising) Rust 实战:构建实用的 CLI 工具 HTTPie Rust编程语言学习之高级特性 Rust内存管理揭秘:深度剖析指针与智能指针 解决Rust中数组和切片的编译时大小问题 《Rust编程之道》学习笔记一 Rust Async 异步编程 简易教程 使用 Async Rust 构建简单的 P2P 节点 Rust编程语言入门之模式匹配 Rust async 编程 Rust编程语言之编写自动化测试 Rust编程语言之函数式语言特性:迭代器和闭包 《Rust编程之道》学习笔记二 Rust Tips 比较数值 使用 Rust 开发一个微型游戏 Rust编程初探:深入理解Struct结构体 深入理解Rust中的内存管理:栈、堆与静态内存详解 深入理解 Rust 结构体:经典结构体、元组结构体和单元结构体的实现 深入掌握 Rust 结构体:从模板到实例化的完整指南 深入理解Rust中的结构体:逻辑与数据结合的实战示例 深入理解 Rust 枚举:从基础到实践 掌握Rust字符串的精髓:String与&str的最佳实践 全面解析 Rust 模块系统:实战案例与应用技巧 Rust 中的 HashMap 实战指南:理解与优化技巧 掌握Rust模式匹配:从基础语法到实际应用 Rust 中的面向对象编程:特性与实现指南 深入理解 Rust 的 Pin 和 Unpin:理论与实践解析 Rust Trait 与 Go Interface:从设计到实战的深度对比 从零开始:用 Rust 和 Axum 打造高效 Web 应用 Rust 错误处理详解:掌握 anyhow、thiserror 和 snafu Rust 如何优雅实现冒泡排序 链表倒数 K 节点怎么删?Python/Go/Rust 实战 用 Rust 玩转数据存储:JSON 文件持久化实战 Rust实战:打造高效字符串分割函数 如何高效学习一门技术:从知到行的飞轮效应 Rust 编程入门:Struct 让代码更优雅 Rust 编程:零基础入门高性能开发 用 Rust 写个猜数游戏,编程小白也能上手! Rust 入门教程:变量到数据类型,轻松掌握! 深入浅出 Rust:函数、控制流与所有权核心特性解析 从零开始:用 Rust 和 Axum 打造高效 Web 服务 Rust 集合类型解析:Vector、String、HashMap 深入浅出Rust:泛型、Trait与生命周期的硬核指南 Rust实战:博物馆门票限流系统设计与实现 用 Rust 打造高性能图片处理服务器:从零开始实现类似 Thumbor 的功能 Rust 编程入门实战:从零开始抓取网页并转换为 Markdown 深入浅出 Rust:高效处理二进制数据的 Bytes 与 BytesMut 实战 Rust智能指针:解锁内存管理的进阶之道 用 Rust 打造命令行利器:从零到一实现 mini-grep 解锁Rust代码组织:轻松掌握Package、Crate与Module Rust 所有权:从内存管理到生产力释放 深入解析 Rust 的面向对象编程:特性、实现与设计模式 Rust + Protobuf:从零打造高效键值存储项目 bacon 点燃 Rust:比 cargo-watch 更爽的开发体验 用 Rust 打造微型游戏:从零开始的 Flappy Dragon 开发之旅 函数式编程的Rust之旅:闭包与迭代器的深入解析与实践 探索Rust编程之道:从设计哲学到内存安全的学习笔记 精读《Rust编程之道》:吃透语言精要,彻底搞懂所有权与借用 Rust 避坑指南:搞定数值比较,别再让 0.1 + 0.2 != 0.3 困扰你! 告别 Vec!掌握 Rust bytes 库,解锁零拷贝的真正威力 告别竞态条件:基于 Axum 和 Serde 的 Rust 并发状态管理最佳实践 Rust 异步编程实践:从 Tokio 基础到阻塞任务处理模式 Rust 网络编程实战:用 Tokio 手写一个迷你 TCP 反向代理 (minginx) 保姆级教程:Zsh + Oh My Zsh 终极配置,让你的 Ubuntu 终端效率倍增 不止于后端:Rust 在 Web 开发中的崛起之路 (2024数据解读) Rust核心利器:枚举(Enum)与模式匹配(Match),告别空指针,写出优雅健壮的代码 Rust 错误处理终极指南:从 panic! 到 Result 的优雅之道 想用 Rust 开发游戏?这份超详细的入门教程请收好! 用 Rust 实现 HTTPie:一个现代 CLI 工具的构建过程 Rust 异步实战:从0到1,用 Tokio 打造一个高性能并发聊天室 深入 Rust 核心:彻底搞懂指针、引用与智能指针 Rust 生产级后端实战:用 Axum + sqlx 打造高性能短链接服务 深入 Rust 内存模型:栈、堆、所有权与底层原理 Rust 核心概念解析:引用、借用与内部可变性 掌握 Rust 核心:生命周期与借用检查全解析 Rust 内存布局深度解析:从对齐、填充到 repr 属性 Rust Trait 分派机制:静态与动态的抉择与权衡 Rust Thread::Builder 用法详解:线程命名与栈大小设置 Rust 泛型 Trait:关联类型与泛型参数的核心区别 Rust Scoped Threads 实战:更安全、更简洁的并发编程 Rust 核心设计:孤儿规则与代码一致性解析 Rust 实战:从零构建一个多线程 Web 服务器 Rust Web 开发实战:构建教师管理 API 硬核实战:从零到一,用 Rust 和 Axum 构建高性能聊天服务后端 Rust Web 开发实战:使用 SQLx 连接 PostgreSQL 数据库 硬核入门:从零开始,用 Actix Web 构建你的第一个 Rust REST API (推荐 🔥) Rust 并发编程:详解线程间数据共享的几种核心方法 Rust并发安全基石:Mutex与RwLock深度解析 Rust Web实战:构建优雅的 Actix Web 统一错误处理 煮咖啡里的大学问:用 Rust Async/Await 告诉你如何边烧水边磨豆 深入浅出:Rust 原子类型与多线程编程实践 Rust 并发编程利器:OnceCell 与 OnceLock 深度解析

Rust 并发编程利器:OnceCell 与 OnceLock 深度解析

Rust并发编程利器:OnceCell与OnceLock深度解析在Rust的并发编程世界中,如何安全高效地初始化共享数据是一个常见的挑战。OnceCell和OnceLock作为标准库提供的强大工具,完美地解决了这一问题。它们的核心思想是“一次性”初始化:确保一个值只被设置一次,从

Rust 并发编程利器:OnceCellOnceLock 深度解析

在 Rust 的并发编程世界中,如何安全高效地初始化共享数据是一个常见的挑战。OnceCellOnceLock 作为标准库提供的强大工具,完美地解决了这一问题。它们的核心思想是“一次性”初始化:确保一个值只被设置一次,从而避免了复杂的数据竞争问题。本文将带你深入理解这两个类型,并通过一系列代码示例,探索它们在单线程和多线程环境下的应用。

Rust 多线程:OnceCell & OnceLock

OnceCell<T>

  • OnceCell<T> 在某种程度上是 Cell 和 RefCell 的混合体,用于那些通常只需要设置一次的值。
  • 可以在不移动或复制内部值的情况下获得一个引用 &T
  • 同时也不需要运行时检查
  • 一旦设置后,它的值就不能再被更新,除非你对 OnceCell 本身有一个可变引用

OnceCell 主要方法

  • get:获取内部值的引用
  • set:在值尚未设置时进行设置(返回一个 Result)
  • get_or_init:返回内部值,如果需要则进行初始化
  • get_mut:提供内部值的可变引用,但只有当你对 OnceCell 本身持有一个可变引用时才能使用
  • 因为它是 Cell ,它不是线程安全的

实操

示例一

use std::cell::OnceCell;

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

    let result = cell.set(String::from("hello"));
    assert!(result.is_ok());

    let result = cell.set(String::from("world"));
    assert!(result.is_err());
}

这段代码展示了如何使用 Rust 标准库中的 OnceCell 类型。你可以把它想象成一个“一次性”的容器。


OnceCell 的核心特性是它只能被 成功赋值一次

  • 首先,我们创建了一个空的 OnceCell 实例 cell,此时它里面没有任何值,所以 cell.get().is_none() 返回 true
  • 接着,我们尝试用 String::from("hello") 第一次给它赋值,这个操作是成功的,cell.set() 返回 Ok(()),所以 result.is_ok() 返回 true
  • 最后,我们试图用 String::from("world") 第二次 给它赋值。由于 OnceCell 已经被填满,这次操作会失败,cell.set() 返回一个错误 Err(String::from("world")),所以 result.is_err() 返回 true

简而言之,这段代码通过两次 set 操作,清晰地演示了 OnceCell “只设置一次”的特性。

运行

➜ cargo run
   Compiling cell v0.1.0 (/Users/qiaopengjun/Code/Rust/RustJourney/cell)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/cell`

示例二

use std::cell::OnceCell;

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

    let value = cell.get_or_init(|| "Hello, World!".to_string());
    assert_eq!(value, "Hello, World!");
    assert_eq!(cell.get(), Some(&"Hello, World!".to_string()));
    assert_eq!(cell.get(), Some(value));
    assert!(cell.get().is_some());
}

这段代码展示了 OnceCell 的一个核心功能:get_or_init。你可以把它想象成一个“惰性加载”的单例容器。

OnceCellget_or_init 方法

OnceCell 是一个特殊的容器,它要么是空的,要么只包含一个值。这段代码利用了 get_or_init 方法,这个方法非常实用:它首先会检查 OnceCell 是否已经有值。

  • 如果 没有 值,它就会执行你提供的闭包(即 || "Hello, World!".to_string())来生成一个值,然后把这个新值存储到 cell 中,并返回一个该值的引用。
  • 如果 已经有 值,它就会直接返回现有值的引用,而 不会 再次执行闭包。

通过这种机制,OnceCell 确保了数据只被初始化一次,即使你多次调用 get_or_init,也不会重复创建值。这对于需要进行昂贵或复杂初始化的场景非常有用,因为它保证了性能的同时也确保了数据的单例性。

运行

➜ cargo run
   Compiling cell v0.1.0 (/Users/qiaopengjun/Code/Rust/RustJourney/cell)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/cell`

示例三

use std::cell::OnceCell;

fn main() {
    let mut cell = OnceCell::new();
    let _ = cell.set(String::from("hello"));

    if let Some(value_ref) = cell.get_mut() {
        // *value_ref = "!".to_string();
        // value_ref.push('!');
        value_ref.push_str("!");
    }

    let _ = cell.set(String::from("World"));

    if let Some(value) = cell.get() {
        println!("Value: {value}");
    }
}

这段代码展示了 OnceCell 的一个关键特性:在赋值后仍然可以修改其内部值,但前提是你需要一个可变的 OnceCell 实例。


首先,我们创建一个可变的 OnceCell 实例 cell,并用 "hello" 成功地给它赋了初值。接着,我们使用 cell.get_mut() 方法,这个方法会返回一个可变的引用 Some(&mut String),这允许我们修改 OnceCell 内部的 String 值。在这里,我们将 ! 附加到字符串末尾,使其变为 "hello!"。然后,我们再次尝试用 "World" 赋值,这次操作会失败,因为 OnceCell 只能被成功赋值一次。最后,我们使用 get() 方法获取不可变的引用并打印出最终值,证明了尽管第二次赋值失败了,但对值的修改是成功的,最终输出为 "Value: hello!"

运行

➜ cargo run
   Compiling cell v0.1.0 (/Users/qiaopengjun/Code/Rust/RustJourney/cell)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.40s
     Running `target/debug/cell`
Value: !

RustJourney/cell on  main [!?] is 📦 0.1.0 via 🦀 1.89.0 
➜ cargo run
   Compiling cell v0.1.0 (/Users/qiaopengjun/Code/Rust/RustJourney/cell)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/cell`
Value: hello!

RustJourney/cell on  main [!?] is 📦 0.1.0 via 🦀 1.89.0 
➜ cargo run
   Compiling cell v0.1.0 (/Users/qiaopengjun/Code/Rust/RustJourney/cell)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.29s
     Running `target/debug/cell`
Value: hello!
它只能 set 一次,第二次 set 并没有成功。但是获得 cell 的可变引用后你可以就地修改。

OnceLock<T>

  • 一种同步原语,只能被写入一次(初始化一次)
  • 是线程安全的 OnceCell,可以在 static 中使用
  • 用于线程安全的、一次性的变量初始化,并且可以根据调用者使用不同的初始化函数

示例四

use std::{sync::OnceLock, thread};

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

fn main() {
    assert!(LOCK.get().is_none());
    thread::spawn(|| {
        let value = LOCK.get_or_init(|| 42);
        assert_eq!(*value, 42);
        assert_eq!(value, &42);
        assert_eq!(LOCK.get(), Some(&42));
    })
    .join()
    .unwrap();

    assert_eq!(LOCK.get(), Some(&42));
}

这段代码展示了 OnceLock 的主要用途:在多线程环境下安全地进行一次性初始化OnceLockOnceCell 的线程安全版本,它确保了即使有多个线程同时尝试初始化,也只有一个线程会成功,而其他线程会等待,并最终得到同一个初始化后的值。


代码中,我们定义了一个静态的、全局可用的 OnceLock 变量 LOCK。在主线程中,我们创建了一个新线程。这个新线程中的 LOCK.get_or_init(|| 42) 方法会检查 LOCK 是否已经被初始化:

  • 如果是第一次调用,它就会执行闭包 || 42 来生成值 42,然后将这个值原子性地存储到 LOCK 中,并返回一个对该值的引用。
  • 如果其他线程同时调用 get_or_init,它们会等待第一个线程完成初始化,然后直接获取到同一个值,而不会重复执行初始化逻辑。

最后,当子线程执行完毕并返回主线程后,主线程通过 assert_eq!(LOCK.get(), Some(&42)) 验证了 LOCK 已经成功被初始化,并包含了 42 这个值。这证明了 OnceLock 能够跨线程安全地完成一次性初始化。

运行

➜ cargo run
   Compiling cell v0.1.0 (/Users/qiaopengjun/Code/Rust/RustJourney/cell)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
     Running `target/debug/cell`

可以看到 OnceLock 是线程安全的,而 OnceCell 不是。

示例五

use std::{
    sync::{
        OnceLock,
        atomic::{AtomicU32, Ordering},
    },
    thread,
};

static LIST: OnceList<u32> = OnceList::new();
static COUNTER: AtomicU32 = AtomicU32::new(0);

const LEN: u32 = 1000;

fn main() {
    thread::scope(|s| {
        for _ in 0..thread::available_parallelism().unwrap().get() {
            s.spawn(|| {
                while let i @ 0..LEN = COUNTER.fetch_add(1, Ordering::Relaxed) {
                    LIST.push(i);
                }
            });
        }
    });

    for i in 0..LEN {
        assert!(LIST.contains(&i));
    }
}

struct OnceList<T> {
    data: OnceLock<T>,
    next: OnceLock<Box<OnceList<T>>>,
}

impl<T> OnceList<T> {
    const fn new() -> OnceList<T> {
        OnceList {
            data: OnceLock::new(),
            next: OnceLock::new(),
        }
    }

    fn push(&self, value: T) {
        if let Err(value) = self.data.set(value) {
            let next = self.next.get_or_init(|| Box::new(OnceList::new()));
            next.push(value);
        }
    }

    fn contains(&self, example: &T) -> bool
    where
        T: PartialEq<T>,
    {
        self.data
            .get()
            .map(|value| value == example)
            .filter(|v| *v)
            .unwrap_or_else(|| {
                self.next
                    .get()
                    .map(|next| next.contains(example))
                    .unwrap_or(false)
            })
    }
}

这段代码定义并使用了一个名为 OnceList 的自定义数据结构,它利用 OnceLock原子操作,在多线程环境下构建了一个线程安全且只支持尾部追加的链表

代码的核心思想是:

  1. OnceList 结构: 每个 OnceList 节点都包含一个 OnceLock 来存储当前节点的值,以及另一个 OnceLock 来存储下一个节点的指针。OnceLock 的“只设置一次”特性保证了每个节点的值一旦被设置,就不会被修改,从而避免了数据竞争。
  2. push 方法: 当多个线程同时调用 push 方法时,它们会尝试给当前的 OnceList 节点赋值。只有一个线程会成功(self.data.set(value) 返回 Ok),而失败的线程(self.data.set(value) 返回 Err)则会通过 get_or_init 方法安全地获取或创建下一个节点,并将自己的值推送到下一个节点。这个过程会递归地进行,直到找到一个空的节点来存储值,从而确保所有线程都能安全地向列表中添加元素。
  3. 多线程环境: main 函数利用 thread::scopeAtomicU32 来生成一系列不重复的数字,并让多个线程并发地将这些数字推入 OnceList。即使多个线程同时对同一个节点进行操作,OnceLock 的内部机制也保证了操作的原子性和线程安全性。

最终,main 函数通过断言 (assert!) 验证了所有从 0 到 999 的数字都成功地被添加到了 OnceList 中,证明了这种基于 OnceLock 的链表实现在高并发场景下是正确且有效的。

运行

➜ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/cell`

总结

OnceCellOnceLock 是 Rust 并发编程中不可或缺的工具。它们的设计理念简洁而强大:只允许一次成功设置,从而根本上杜绝了重复初始化和数据竞争的可能。

  • OnceCell 适用于单线程环境,是实现惰性加载和单例模式的理想选择。尽管它不能在多个线程间共享,但它提供了便捷的 API,如 get_or_init,让你可以优雅地初始化和访问值。
  • OnceLock 则将这一概念提升到线程安全层面。它保证了即使在最复杂的并发场景下,静态变量或全局资源也只会被初始化一次。这对于数据库连接、缓存池或任何昂贵的全局资源初始化都至关重要。

通过巧妙地结合 OnceLock 和原子操作,我们甚至可以构建出复杂的、线程安全的数据结构,如示例中的 OnceList。掌握了这两个工具,你就掌握了在 Rust 中进行安全、高效、并发初始化的核心方法。

参考

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论