跳转至

19 海纳百川,有容乃大:并发容器(上)

你好,我是鸟窝。

从这节课开始,我们将进入新的篇章,重点学习Rust中标准库的同步原语。

Rust的同步原语体系丰富而强大,为了便于系统性理解,我们可以将其分为几种类型分别进行讲解。接下来这两节课将重点介绍容器类(或者叫做包装类)并发原语,它们的共同特点是通过对普通数据的包装,来提供更丰富的功能特性。这些容器包括 CowBoxCellRefCellOnceCellLazyCellRcArc 等。它们很多都不是线程安全的,但是常常会在并发程序中使用。

本节课的内容比较详细,内容很多,所以我分成两节课来介绍。

有些文章把它们叫做智能指针,我认为叫什么都不重要,重要的是我们能够了解它们并且能够熟练运用。

这些容器类并发原语各具特色:Cow(Clone-on-Write)提供了写时克隆的能力,可以在需要修改数据时才进行复制,从而优化性能;beef::Cow 则是对标准库 Cow 的一个优化实现。Box 作为 Rust 中最基础的智能指针,将数据存储在堆上;而 CellRefCell 则提供了内部可变性,使得我们能够在不可变引用的情况下修改数据。

在延迟初始化方面,OnceCellLazyCell 提供了不同层次的支持。OnceCell 确保值只被初始化一次;LazyCell 则在第一次访问时才进行初始化。在共享数据方面,Rc(引用计数)允许在单线程环境下安全地共享数据,而 Arc(原子引用计数)则通过原子操作提供了线程安全的共享机制,使得数据可以安全地在多个线程间传递和访问。

这些容器虽然功能各异,但都遵循 Rust 的所有权系统和借用规则,在提供额外功能的同时确保了内存安全和线程安全。通过合理使用这些容器,我们可以更优雅地处理复杂的并发场景,实现更高效的资源管理。值得注意的是,RcArc 虽然都提供了共享所有权的能力,但 Arc 因为使用原子操作而有额外的性能开销,因此在单线程环境下应优先使用 Rc

这些容器类并发原语的设计体现了 Rust 在类型系统和内存管理上的深思熟虑。它们不仅提供了强大的功能,还通过编译时检查确保了使用的正确性。理解并掌握这些容器的使用,是构建高质量 Rust 并发程序的基础。

Cow

Cow(Clone-on-Write)是Rust标准库中提供的一个智能指针类型,全称为 std::borrow::Cow,是一种提供写时复制功能的智能指针。它是一个枚举类型,可以持有数据的借用或者拥有的版本。Cow 的独特之处在于它只在需要修改数据时才会克隆数据,这使得它在处理只读场景时非常高效

图片

该类型旨在通过 Borrow trait 处理通用的借用数据。Cow 实现了 Deref,这意味着你可以直接在其封装的数据上调用非修改方法。如果需要修改,to_mut 将获取一个可变引用的所有权值,并在必要时进行克隆。而在需要引用计数指针的场景下,请注意 Rc::make_mutArc::make_mut 也可以提供按需克隆功能。

Cow 的定义如下:

pub enum Cow<'a, B>
where
    B: 'a + ToOwned + ?Sized,
{
    Borrowed(&'a B),
    Owned(<B as ToOwned>::Owned),
}
  • Borrowed:持有数据的引用。
  • Owned:持有数据的所有权。

创建Cow

下面这个例子演示了三种生成 Cow 类型的不同方式:

use std::borrow::Cow;

fn main() {
   // 从借用创建
   let borrowed: Cow<str> = Cow::Borrowed("hello");
   println!("is_borrowed: {}, is_owned: {}", borrowed.is_borrowed(),borrowed.is_owned()); // true,false

   // 从拥有的数据创建
   let owned: Cow<str> = Cow::Owned(String::from("hello"));
   println!("is_borrowed: {}, is_owned: {}", owned.is_borrowed(),owned.is_owned()); // false,true


   // 从引用创建(自动选择)
   let text = "hello";
   let cow: Cow<str> = text.into();
   println!("is_borrowed: {}, is_owned: {}", cow.is_borrowed(),cow.is_owned()); // true,false

}

into_owned 方法

这个方法将获取所有权数据。若数据当前为借用状态,则执行克隆操作。

下面这个例子是原数据处于借用状态,所以会执行克隆操作:

use std::borrow::Cow;

let s = "Hello world!";
let cow = Cow::Borrowed(s);

assert_eq!(cow.into_owned(),String::from(s));

下面这个例子原数据是有所有权的,所以直接从 Cow move出去,不需要克隆操作:

use std::borrow::Cow;

let s = "Hello world!";
let cow: Cow<'_, str> = Cow::Owned(String::from(s));

assert_eq!(cow.into_owned(),String::from(s));

to_mut 方法

取得数据所有权形式的可修改引用。若数据当前为借用状态,则执行克隆操作。

下面这个例子取得数据的所有权,并返回了它的可修改引用,并修改为大写字母。

let mut cow = Cow::Borrowed("foo");
cow.to_mut().make_ascii_uppercase();
assert_eq!(cow, Cow::Owned(String::from("FOO")) as Cow<'_, str>);
assert_eq!(true, cow.is_owned());

使用 Cow 时,不要过早地进行克隆,这就失去了 Cow 的意义。

// 不好的做法
let cow:Cow<str>=Cow::Owned(string.clone());

// 好的做法
let cow:Cow<str>= string.into();

而且,一般情况下,如果你不需要修改,就不要调用 to_mut 方法。

// 不好的做法
cow.to_mut();// 即使不修改也会克隆

// 好的做法
if need_modification {    
  cow.to_mut().push_str("suffix");
 }

beef::Cow

beef::Cow 是一个比标准库 Cow 更快、更紧凑的实现。该 crate 包含了两个版本的 Cow

  • beef::Cow 是 3 个字宽:指针、长度和容量。它将所有权标签存储在容量中。
  • beef::lean::Cow 是 2 个字宽,将长度、容量和所有权标签都存储在一个字中。

官方称 beef::Cow 的两个版本都比 std::borrow::Cow 更精简,但实际上标准库中的 Cow 同样是三个字宽。由于 beef 库已有两年未更新,而 Rust 标准库在持续发展,这种说法可能存在一定局限性。

    const WORD: usize = size_of::<usize>();

    assert_eq!(size_of::<std::borrow::Cow<str>>(), 3 * WORD);
    assert_eq!(size_of::<beef::Cow<str>>(), 3 * WORD);
    assert_eq!(size_of::<beef::lean::Cow<str>>(), 2 * WORD);

它的使用方法和标准库中的 Cow 使用方法类似:

use beef::Cow;

let borrowed: Cow<str> = Cow::borrowed("Hello");
let owned: Cow<str> = Cow::owned(String::from("World"));

assert_eq!(
    format!("{} {}!", borrowed, owned),
    "Hello World!",
);

Box

Box 是Rust标准库中最简单也是最常用的智能指针。它提供了最基础的堆内存分配能力,允许我们将数据存储在堆上而不是栈上。Box 的所有权语义遵循Rust的基本规则:当 Box 离开作用域时,它的析构函数会被调用,Box 所指向的堆内存会被自动释放。

图片

Box类型拥有下面的特点:

  • 堆内存分配:Box 的最基本特点是提供堆内存分配能力。
// 在栈上分配
let stack_data = 42;

// 在堆上分配
let heap_data = Box::new(42);

// 堆上分配大型数据结构
let large_array = Box::new([0; 1000000]); // 在堆上分配大数组
  • 零运行时开销:除了堆分配外没有额外开销。

没有额外的运行时开销:

usestd::mem::size_of;

// 在64位系统上
assert_eq!(size_of::<Box<i32>>(),size_of::<usize>());// 通常是8字节

直接映射到裸指针:

let boxed =Box::new(42);
let raw_ptr =Box::into_raw(boxed);
let back_to_box =unsafe{Box::from_raw(raw_ptr)};
  • 实现了Deref/DerefMut特质:允许透明地访问内部值。
struct MyStruct {
    value: i32,
}

impl MyStruct {
    fn get_value(&self) -> i32 {
        self.value
    }
}

let my_box = Box::new(MyStruct { value: 42 });

// 无需显式解引用就能调用方法
let val = my_box.get_value();

// 显式解引用也可以
let val = (*my_box).get_value();

// 可以直接访问字段
println!("Value: {}", my_box.value);
  • 所有权语义Box 遵循 Rust 的所有权规则。
fn take_ownership(boxed: Box<i32>) {
    println!("Value: {}", *boxed);
} // boxed在这里被释放

fn main() {
    let my_box = Box::new(42);
    take_ownership(my_box);
    // println!("{}", *my_box); // 编译错误:my_box已被移动
}

下面是借用的规则:

fn borrow_box(boxed: &Box<i32>) {
    println!("Borrowed value: {}", **boxed);
}

fn mut_borrow_box(boxed: &mut Box<i32>) {
    **boxed += 1;
}

fn main() {
    let mut my_box = Box::new(42);
    borrow_box(&my_box);
    mut_borrow_box(&mut my_box);
}
  • 编译时大小确定性:因此 Box 允许创建递归和动态大小类型。
// 递归类型
enum List {
    Cons(i32, Box<List>),
    Nil,
}

// 动态大小类型
trait Animal {
    fn make_sound(&self);
}

struct Dog;
impl Animal for Dog {
    fn make_sound(&self) {
        println!("Woof!");
    }
}

// 可以创建特质对象
let animal: Box<dyn Animal> = Box::new(Dog);
  • Drop语义Box 实现了 Drop 特质,确保资源正确释放。
struct CustomType {
    data: String,
}

impl Drop for CustomType {
    fn drop(&mut self) {
        println!("Dropping CustomType with data: {}", self.data);
    }
}

fn main() {
    let boxed = Box::new(CustomType {
        data: "hello".to_string(),
    });
    // 当boxed离开作用域时,
    // 1. 首先调用CustomType的drop
    // 2. 然后释放堆内存
}
  • 线程间传递:如果Box内容是 SendBox 可以在线程间安全传递。
use std::thread;

fn main() {
    let boxed = Box::new(42);
    
    let handle = thread::spawn(move || {
        println!("Value in new thread: {}", *boxed);
    });
    
    handle.join().unwrap();
}

Cell、RefCell、OnceCell和LazyCell

Cell

Rust的所有权与借用系统是其安全性与线程安全性的核心。然而,有时候我们需要在不违反借用规则的情况下修改数据。这就是内部可变性(Interior Mutability)的概念,而 Cell<T> 是实现这一概念的基础类型之一。

图片

Cell<T> 是Rust标准库中的一个类型,位于 std::cell 模块,它提供了一种在拥有不可变引用的同时修改其内部值的方法。简单来说,Cell<T> 允许我们“绕过”借用检查器的某些限制,但仍然保持内存安全。通过将值移入和移出单元格来实现内部可变性。

在Rust中,如果你有一个不可变引用(&T),你不能通过它修改数据。这是Rust借用规则的核心部分。但有时,我们需要修改某个字段,同时保持结构体的其他部分不可变。这就有了 Cell<T> 的用武之地。

struct Counter {
    value: Cell<i32>,
}

impl Counter {
    fn new() -> Self {
        Counter { value: Cell::new(0) }
    }
    
    fn increment(&self) {
        let current = self.value.get();
        self.value.set(current + 1);
    }
}

在上面的例子中,尽管 increment 方法接收的是 &self(不可变引用),但我们仍然可以修改 value 字段,因为它被包裹在 Cell 中。

Cell 就像一个特殊的透明保险柜。想象一下,这个保险柜有一个特殊设计——它的外壳是完全透明的,让你可以看见里面存放的物品,就像你可以通过不可变引用“看到”Cell中的值,但它有一个特殊的机制:

  • 对于小物品(实现了Copy特性的类型),保险柜有一个自动复制装置。当你想取出物品时,它会自动复制一份给你,而原物品依然安全地保存在里面,这就是 get() 方法。
  • 对于任何物品,保险柜有一个特殊的投递口。你可以通过这个口把新物品放进去,同时旧物品会被替换出来,这就是 replace() 方法。
  • 有时你只想更新里面的物品,不需要取出来。这时候,你可以通过一个特殊的操作窗口修改物品,这就是 update() 方法。
  • 最特别的是,即使你只有这个保险柜的查看权限(不可变引用),你依然可以通过这些机制改变里面的内容。

这个保险柜只能放在一个房间里(单线程环境),如果你想在多个房间之间共享,就需要使用更复杂的保险系统(比如Mutex)。而且,对于大型物品(没有实现Copy特性的类型),你不能直接复制,需要使用一个更高级的保险柜(RefCell)。

Cell<T> 适用于以下场景:

  • 需要内部可变性:当你想在拥有不可变引用的同时修改数据。
  • 简单数据类型Cell<T> 最适合用于实现了 Copy 特性的类型,如整数、浮点数、布尔值等,虽然实际上 T 并没有限制。
  • 非线程安全场景Cell<T> 不是线程安全的,它仅适用于单线程环境。
  • 避免借用检查冲突:在可能出现多个可变借用的情况下,使用 Cell<T> 可以避免编译错误。

接下来让我们学习 Cell<T> 的使用方法。

  1. new 创建新的Cell

创建一个新的 Cell,其中包含给定的值。

// 创建新的Cell
let c = Cell::new(5);
  1. get 获取Cell中的值

返回所包含值的副本:

let c = Cell::new(5);
let value = c.get(); // 返回5

如果包含的类型T没有实现Copy 特质,那么调用get方法时编译失败:

use std::cell::Cell;

fn main() {
    let my_cell = Cell::new(String::from("hello")); // String 没有实现 Copy

    let value = my_cell.get(); // 编译错误!
    println!("Value: {}", value);
}
  1. get_mut 返回底层数据的可变引用

此调用(在编译时)可变地借用 Cell,这保证了我们拥有唯一的引用。但是请注意:此方法期望 self 是可变的,通常在使用 Cell 时不是这种情况。如果需要通过引用进行内部可变性,你可以考虑使用 RefCell,它通过其 borrow_mut 方法提供运行时检查的可变借用。

use std::cell::Cell;

let mut c = Cell::new(5);
*c.get_mut() += 1;

assert_eq!(c.get(), 6);

所以如果你没有特殊的需求,这个方法你直接忽略就好。

反向操作就是 from_mut 方法:从 &mut T 返回 &Cell<T>

  1. set 设置Cell的新值
let c = Cell::new(5);
c.set(10); // 现在c包含10
  1. update 使用函数更新包含的值,并返回新值
let c = Cell::new(5);
c.update(|x| x + 1); // c现在包含6
  1. replace 获取并替换值

val 替换内部值,并返回被替换的值。

let c = Cell::new(5);
let old_value = c.replace(10); // 返回5,c现在包含10
  1. swap 交换两个Cell的值

std::mem::swap 的区别在于,此函数不需要 &mut 可变引用。

如果 selfother 是部分重叠的不同 Cell,则此函数会引发恐慌。仅使用标准库方法,不可能创建这种部分重叠的 Cell。但是,允许不安全代码创建例如两个部分重叠的 &Cell<[i32; 2]>

  1. take 取出Cell的值

获取单元格的值,并将单元格替换为 Default::default()

use std::cell::Cell;

let c = Cell::new(5);
let five = c.take();

assert_eq!(five, 5);
assert_eq!(c.into_inner(), 0);
  1. into_inner 获取底层数据

提取值,并销毁单元格。

use std::cell::Cell;

let c = Cell::new(5);
let five = c.into_inner();

assert_eq!(five, 5);

还有仅限 Nightly 版本的实验性 API,它能将元素为T的数组或者切片转换成 Cell<T> 的数据和切片。

as_ptr 返回指向此单元格底层数据的原始指针。

Cell<T> 的大小基本上就是T的大小,没有显著的内存开销。可以通过 std::mem::size_of 来验证:

use std::cell::Cell;
use std::mem::size_of;

fn main() {
    println!("size of i32: {}", size_of::<i32>());  // 通常是4字节
    println!("size of Cell<i32>: {}", size_of::<Cell<i32>>()); // 也是4字节
}

RefCell

RefCell 是内部可变性类型的另一个实现。在标准的Rust借用规则下,如果你有一个不可变引用(&T),你不能通过它修改数据。但在某些情况下,我们确实需要这样的能力,例如:

  • 当一个对象的方法需要修改自身,但该方法只接受不可变引用(&self)时;
  • 在实现某些特定的数据结构(如缓存、图等)时;
  • 当我们希望有选择地违反借用规则,但仍然保证安全时。

图片

以下是一个简单的例子:

struct Logger {
    messages: RefCell<Vec<String>>,
}

impl Logger {
    fn new() -> Self {
        Logger {
            messages: RefCell::new(Vec::new()),
        }
    }

    fn log(&self, message: &str) {
        self.messages.borrow_mut().push(message.to_string());
    }

    fn get_messages(&self) -> Vec<String> {
        self.messages.borrow().clone()
    }
}

在这个例子中,尽管 log 方法只接受不可变引用(&self),我们仍然可以修改 messages 字段,因为它被包裹在 RefCell 中。

RefCell 就像一个更先进的保险柜,拥有以下特点:

  • 智能访问控制系统:这个保险柜不像Cell那样简单透明,而是配备了一个复杂的访问控制系统。它会记录谁正在访问保险柜内容,以及访问的方式(查看还是修改)。
  • 双模式访问:保险柜有两种开启模式。

  • 查看模式(borrow()):可以同时发放多张查看卡,允许多人同时查看内容,但不能修改。

  • 修改模式(borrow_mut()):只能发放一张修改卡,允许持卡人修改内容,但此时不能发放任何查看卡。
  • 动态警报系统:如果有人试图违反规则,例如,在已经有人拿到修改卡的情况下再请求查看卡,保险柜会立即触发警报(panic)。
  • 通用存储能力:与只适合存放小物品(Copy类型)的Cell不同,RefCell可以安全地存放任何大小和类型的物品。
  • 访问凭证:当你请求访问保险柜时,它不会像Cell那样直接给你物品的副本,而是发给你一个特殊的访问凭证(Ref或RefMut),让你通过这个凭证来访问或修改内容。
  • 自动凭证回收:当你用完访问凭证离开时,系统会自动收回凭证,重置柜内物品的访问状态。
  • 访问检查开销:这种智能系统需要额外的电力和计算能力来运行(运行时检查开销),这是相比简单透明保险柜(Cell)的一个缺点。

与Cell类似,这种保险柜也只能安装在一个安全室内(单线程环境)。如果需要在多个安全室之间共享,则需要更高级的带锁保险系统(如Mutex)。

RefCell<T> 适用于以下场景:

  • 需要内部可变性:当你需要在拥有不可变引用的同时修改数据。
  • 单线程环境RefCell<T> 不是线程安全的,只适用于单线程环境。
  • 需要运行时借用检查:当编译时借用检查太严格,而你确信你的代码是安全的。

接下来让我们学习 RefCell<T> 的使用方法。

  1. new 创建新的RefCell
// 创建新的RefCell
let ref_cell = RefCell::new(5);
  1. borrow 借用内部值(不可变借用)
let ref_cell = RefCell::new(5);
let borrowed = ref_cell.borrow(); // 返回Ref<T>
println!("Value: {}", *borrowed);
  1. borrow_mut 可变借用内部值
let ref_cell = RefCell::new(5);
let mut borrowed_mut = ref_cell.borrow_mut(); // 返回RefMut<T>
*borrowed_mut += 1;
println!("New value: {}", *borrowed_mut);

borrow_mut() 返回 RefMut<T>,也实现了 DerefDerefMut 特性。

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);
    
    // 获取RefMut<Vec<i32>>
    let mut data_ref_mut = data.borrow_mut();
    
    // 使用Deref特性访问内部值
    println!("Vector length: {}", data_ref_mut.len());  // 等同于 (*data_ref_mut).len()
    
    // 使用DerefMut特性修改内部值
    data_ref_mut.push(4);  // 等同于 (*data_ref_mut).push(4)
    
    // 现在vector是[1, 2, 3, 4]
    println!("Vector: {:?}", *data_ref_mut);
}
  1. try_borrow 尝试借用(不会panic)
let ref_cell = RefCell::new(5);
let borrow_result = ref_cell.try_borrow(); // 返回Result<Ref<T>, BorrowError>
if let Ok(borrowed) = borrow_result {
    println!("Successfully borrowed: {}", *borrowed);
} else {
    println!("Failed to borrow");
}
  1. try_borrow_mut 尝试可变借用(不会panic)
let ref_cell = RefCell::new(5);
let borrow_mut_result = ref_cell.try_borrow_mut(); // 返回Result<RefMut<T>, BorrowMutError>
if let Ok(mut borrowed) = borrow_mut_result {
    *borrowed += 1;
    println!("Successfully mutably borrowed: {}", *borrowed);
} else {
    println!("Failed to mutably borrow");
}
  1. as_ptr 获取内部值的可变引用(不检查借用规则)
let ref_cell = RefCell::new(5);
let value = unsafe { &mut *ref_cell.as_ptr() };
*value += 1;
println!("Value: {}", ref_cell.borrow());
  1. into_inner 消费RefCell并返回内部值
let ref_cell = RefCell::new(5);
let value = ref_cell.into_inner(); // 返回5
  1. 获取RefCell内部的借用状态
let ref_cell = RefCell::new(5);
let _borrow = ref_cell.borrow_mut();
println!("Borrow state: {:?}", ref_cell.try_borrow()); // Prints: Err(BorrowError)

同样的,RefCell<T> 提供了和 Cell<T> 类似的方法,比如 get_mutswaptakereplace 以及新增加的 replace_with 等,这里就不再赘述了,我只介绍新的 replace_with

  1. replace_with 使用函数替换包含的值,并返回新值

使用 f 计算出的新值替换封装的值,返回旧值,且不进行任何值的析构。

不清楚为啥不和Cell的update方法名保持一致。

use std::cell::RefCell;
let cell = RefCell::new(5);
let old_value = cell.replace_with(|&mut old| old + 1);
assert_eq!(old_value, 5);
assert_eq!(cell, RefCell::new(6));

OnceCellLazyCellRc 的介绍我们放在下一节课中介绍。

总结

好了,在这一节课中,我们相当全面地了解了 CowBoxCellRefCell 几种容器(包装器)。只有熟悉这些数据结构的功能和应用场景,你才会得心应手地使用它们。

下面是这几种常用数据类型的功能与应用场景的概览:

思考题

你正在实现一个日志过滤器模块,它的功能是扫描一段日志内容,并将所有敏感词替换成星号

例如敏感词是:"ERROR""PANIC",日志内容是:"System ERROR occurred. Please check for PANIC.",处理后应为:"System ***** occurred. Please check for *****."

关键要求

  • 如果日志内容不包含敏感词,直接返回原始日志内容的引用(Borrowed),不拷贝。
  • 如果日志内容包含敏感词,返回一个新字符串(Owned),其中敏感词已替换。
  • 敏感词可能非常多,因此函数要支持传入一个动态敏感词列表,而不是写死。

方法签名

fn filter_sensitive<'a>(log: &'a str, sensitive_words: &[&str]) -> std::borrow::Cow<'a, str>

示例

let log = "System ERROR occurred. Please check for PANIC.";
let sensitive = &["ERROR", "PANIC"];

assert_eq!(
    filter_sensitive(log, sensitive),
    Cow::Owned("System ***** occurred. Please check for *****.".to_string())
);

let clean_log = "All systems operational.";
assert_eq!(
    filter_sensitive(clean_log, sensitive),
    Cow::Borrowed("All systems operational.")
);

欢迎你把你实现的结果分享到留言区,如果你觉得这节课的内容对你有帮助的话,也欢迎你分享给需要的朋友,我们下节课再见!

精选留言(1)
  • soddygo 👍(0) 💬(1)

    Cell<T> 类型,T 必须实现 Copy trait,一般是基础类型,比如 bool,u8等; RefCell<T> ,T 不要求实现Copy,可以是任意类型,比如数组,通过引用形式,来修改内部的数组; 这么记,更容易区分 Cell和RefCell的使用场景

    2025-05-15