Rust中的智能指针

file:///Users/fliter/rust-in-databend/17-%E6%8E%A2%E8%AE%A8Rust%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88/%E6%8E%A2%E8%AE%A8Rust%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88.pdf

file:///Users/fliter/rust-in-databend/19-%E6%8E%A2%E8%AE%A8Rust%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88II/%E6%8E%A2%E8%AE%A8Rust%E6%99%BA%E8%83%BD%E6%8C%87%E9%92%88%202.pdf


Box、Vec | String、Cell | RefCell、Rc | Arc、RwLock | Mutex 都是什么?有何作用


这些Rust中的常见智能指针和同步原语都有以下作用:

  • Box: 将数据放在heap中,实现类似C++指针的功能。

  • Vec: 动态数组,内部使用Box将每个元素放入heap。

  • String: 可增长的UTF-8字符串,内部使用Vec存储字符数据。

  • Cell: 线程安全的紧耦合内存类型,可用于在线程间传递值。

  • RefCell: 运行时检查借用规则的智能指针,可在单线程下解决borrows问题。

  • Rc: 引用计数指针,简化cloned值的分享工作。

  • Arc: 可在线程间共享的Rc智能指针。

  • RwLock: 读写锁,可实现多线程下只读共享、写独占的安全访问数据結构模式。

  • Mutex: 互斥锁,保证对共享数据的独占访问。

它们主要用于解决Rust特有的内存管理问题,比如在堆上分配数据、实现共享数据访问、打破借用规则等。
为线程安全程序和多线程同步提供支持。


能给我举几个使用这些智能指针和同步原语的例子吗?


这里举几个使用Rust智能指针和同步原语的简单例子:

  1. 使用Box将结构体放在堆上:
1
let b = Box::new(MyStruct { foo: "hello" });
  1. 使用Rc实现引用计数:
1
2
let r = Rc::new(String::from("hi"));
let r2 = r.clone();
  1. 使用Mutex进行互斥操作:
1
2
3
let mutex = Mutex::new(vec![1,2,3]);
let mut locked = mutex.lock().unwrap();
locked.push(4);
  1. 使用Cell进行线程安全访问:
1
2
3
let data = Cell::new(5); 
data.set(10);
println!("Count is {}", data.get());
  1. 使用RefCell进行借用检查:
1
2
3
let r = RefCell::new(vec![1,2,3]);
let b = r.borrow();
println!("Vector contains {:?}", b);
  1. 使用Arc和RwLock进行多线程访问:
1
let data = Arc::new(RwLock::new(vec![1,2,3]));

这些例子阐明了它们各自的用途。


能解释一下RefCell的作用是什么吗?


RefCell 在 Rust 中有以下几个主要作用:

  • 运行时借用检查:RefCell 能在运行时检查和执行借用规则,而不是编译时检查。

  • 宏观共享:允许在不需要线程安全的单线程情况下,宏观上共享数据而不考虑微观借用规则。

  • 打破借用限制:RefCell 允许发生 ABA 问题,比如允许同时存在不可变和可变引用,这在编译时是不允许的。

  • 实现 Interior Mutability:通过 RefCell 将内部可变性从外部暴露为不可变引用,达到通过不可变借用外部访问可变内部数据的目的。

  • 编译时不安全为运行时安全:编译时无法检测的视情况,通过运行时检查避免发生错误。

  • 补充 Mutex:比 Mutex 更轻量,用于暴露内部可变性而不是全局互斥锁。

总之,RefCell 通过运行时借用检查,可以在一定程度上弥补 Rust 编译时借用规则的局限性,让一些模式可以工作,从而增加了应用层次的灵活性。但运行时性能比 Mutex 更差。

RefCell 是否线程安全?

RefCell 本身不提供线程安全的保证,它仅仅在单线程下执行运行时的借用检查。

具体来说:

  • RefCell 内部数据的读写访问是非线程安全的,可能会发生数据竞争导致错误。

  • 它无法同步多个线程对内部数据的访问,一个线程在修改期间,其他线程可能获得了无效的引用。

  • 即使在单线程下使用 RefCell,发生死锁也无法检测和预防。

  • 通过 RefCell 获取的引用在不同线程中使用,也可能违反其内部的借用规则。

所以为了使 RefCell 的数据在多线程环境下得到线程安全访问,需要额外使用同步原语:

  • 使用 Mutex<RefCell> 实现互斥访问保证安全。

  • 使用 RwLock<RefCell> 实现可读可写模式的线程安全访问。

  • 使用 Arc<RefCell> 在线程间共享不可变引用。

所以总体来说,RefCell 自身不提供线程安全能力,还需结合 Rust 其他同步原语来同步多线程访问,才能在多线程环境下正常工作。


RefCell 和 Mutex 有什么区别?


RefCell 和 Mutex 在 Rust 中有以下主要区别:

  • 适用场景:

    RefCell 主要用于程序内部打破编译时借用规则,实现内部可变性。

    Mutex 用于多线程环境下的互斥访问,提供全局锁保证同步。

  • 性能:

    RefCell 采用运行时检查,性能开销小于 Mutex。

    Mutex 内部使用原子操作进行锁操作,运行开销较大。

  • 访问模式:

    RefCell 允许同时存在可变和不可变引用。

    Mutex 一次只允许一个可变引用,其他线程阻塞等待。

  • 线程安全性:

    RefCell 本身不提供线程安全保证,需要额外采用同步原语。

    Mutex 直接提供互斥访问的线程安全保证。

  • 用于场景:

    RefCell 用于内部可变性、管理内部引用计数等。

    Mutex 一般用于实现参考计数、同步容器访问等全局互斥。

总的来说:

RefCell 适用于单线程局部内部可变性;Mutex 用于多线程环境下提供全局互斥访问保护内存一致性。它们在适用场景和实现机制上有明显区别。


RefCell 在什么情况下会发生 ABA 问题?


RefCell 在下列情况下可能会发生 ABA 问题:

  • 当其他线程在 RefCell 上执行借用的过程中,内部数据发生了修改。

例如:

1
2
3
thread1 通过 borrow() 获得不可变引用 A;
thread2 修改 RefCell<T> 中的值;
thread1 通过返回的引用 A 还是能访问到值,但值此时已经发生修改。

这种情况下引用 A 看起来还有效,但实际上数据已经变了,违反了借用的目的。

  • 当一个线程同时获得不可变和可变引用时,另一个线程再次修改数据。

例如:

1
2
3
thread1 通过 borrow() 获得不可变引用 A;
thread1 通过 borrow_mut() 又获得可变引用 B;
thread2 修改 RefCell 值;

此时 A 和 B 的有效性已无法保证。

  • 使用 Cell/RefCell 去实现类似 Atomic 数据结构时,可能会面临 ABA 问题。

所以总体来说,只要在 RefCell 借用的过程中,数据有可能被修改,或者同时存在不同类型的借用,就可能会产生 ABA 问题。Rust 无法在编译期检测这类情况。




2024.07.09

https://fenix0.com/smart-pointer/

引用和智能指针一个重要区别是,引用借用数据,智能指针大多情况下拥有数据。

从官方定义来看,智能指针重要特征是实现了Deref和Drop。

  • 解引用deref(*)是引用(&)的反操作。如果我们对一个引用 使用*操作符,将读到地址指向的内存 (*操作符deref还是有一点点区别的)

  • Drop trait 类似结构体生命周期结束的回调


Box 将数据保存在堆上,是最常见的智能指针