Rust 智能指针
Rust 智能指针
2024-03-13
Rust
指针: 就是一个变量, 只不过这个变量里面保存的是其它变量的内存地址.
变量 s 就是一个指针, 里面保存了 String 值的内存地址.

智能指针
智能指针其实就是一个结构体.
它里面保存了一个指针, 然后再根据不同的智能指针类型, 保存一些相关的数据, 比如数据长度、容量、引用计数等等.
然后又实现了三个 trait:
- Defef
- DefefMut
- Drop

智能指针和多线程

Box
先看一下, 下面代码的内存分配:
struct test { num :i32, s1 :String, } fn main() { let t = test{num: 10, s1: String::from("value")}; // ... }
test 本身 (结构体实例的内存布局) 是分配在栈内存, num、s1 的指针、len、cap 也是保存在栈内存的.

但是可以通过 Box 将 test 实例分配到堆内存.
Box 只是将数据分配到堆内存, 然后使用数据的时候 Box 会自动解引用.
所以 Box 是不是线程安全的和它的泛型参数有关系, 如果 Box 的泛型是线程安全的, 那么 Box 就是线程安全的.
// Box::new 会返回一个指向 test 实例的一个指针, // 然后把这个指针保存在栈内存上. let test = Box::new(test{num:10, s1:String::from("")}); // 也可以将数字、字符等放到堆内存上. // Box::new(10);

使用:
// 在 Box 实例上使用解引用符号 *, 把里面的堆上的值再次移动回栈上 let boxed: Box<u8> = Box::new(5); let val: u8 = *boxed; // 这里这个 val 整数实例就是在栈上的值 println!("{:?}", val); println!("{:?}", boxed); // 对于 u8 类型, 解引用后 boxed 实例还能用 // 如果 let b = Box::new(test{num:10, s1:String::from("")}); 包装的是一个结构体, // 那么可以直接通过变量 b 调用结构体中的方法或属性等.
Rc、Arc
- Rc: 非线程安全
- Arc: 线程安全
Arc、Rc 都是用来共享数据的, 但是不能修改这个数据的值.
即使包装的数据是一个可变引用
Arc::new(&mut struct)
也不能修改这个数据的值.每次在使用这个数据的时候, 使用计数都会
+1
, 使用完成后会-1
, 只有等到资源的引用为0
的时候, 才会释放这个资源.
use std::rc::Rc; fn main() { // 也是保存在堆内存. let five = Rc::new(5); // 通过这两种方式都可以增加引用计数, // 这两种方式没有区别, 喜欢哪种用哪种. let _five1 = Rc::clone(&five); let _five2 = five.clone(); // 返回引用计数的数量. println!("Count after cloning: {}", Rc::strong_count(&five)); }
Arc 的使用:
use std::sync::Arc; use std::thread; #[derive(Debug)] struct Test { a: i32, b: i32, } fn main() { let test = Test { a: 1, b: 2 }; // 这里使用 mut 关键字, 没有意义, 因为只能只读. // let mut test_arc = Arc::new(test); // let test_arc_clone1 = Arc::clone(&mut test_arc); let test_arc = Arc::new(test); let test_arc_clone: Arc<Test> = Arc::clone(&test_arc); // 必须使用 move 关键字, 将 test_arc_clone 移动到新线程中, // 防止出现悬垂引用. let handle = thread::spawn(move || { // 第一种使用方式, 直接获取值. // 这种方式就是, 通过 Deref trait「自动解引用」. // let test: Arc<Test> = test_arc_clone; // println!("{}, {}", test.a, test_arc_clone.b); // 第二种使用方式, 需要使用解引用操作符 *「显示解引用」, // 对 Arc<Test> 解引用后数据类型是 Test. // // 但是 Arc 是共享数据, 不能获取所有权, 所以还要使用 & 获取「不可变引用」. let xx: &Test = &*test_arc_clone; println!("{}, {}", xx.a, xx.b); }); handle.join().unwrap(); }
引用循环与内存泄漏
在 Rust 中,使用 Rc<T>
和 RefCell<T>
可以创建循环引用,这可能导致内存泄漏,因为引用计数无法达到零,因此无法自动释放内存。
以下是一个创建循环引用的例子:
use std::rc::Rc; use std::cell::RefCell; struct Node { value: i32, next: RefCell<Option<Rc<Node>>>, } fn main() { let a = Rc::new(Node { value: 1, next: RefCell::new(None), }); let b = Rc::new(Node { value: 2, next: RefCell::new(None), }); // 制造循环引用 *a.next.borrow_mut() = Some(Rc::clone(&b)); *b.next.borrow_mut() = Some(Rc::clone(&a)); // 运行结果: Value a: 1, b: 2 println!("Value a: {}, b: {}", a.value, b.value); // 运行结果: Reference count a: 2, b: 2 // 返回值是 2,这是因为 Rc::new 的时候就会对数据进行一次引用,所以返回值是 2。 println!("Reference count a: {}, b: {}", Rc::strong_count(&a), Rc::strong_count(&b)); // 此时a和b相互引用,无法自动释放 }
为了解决这个问题,我们可以使用 Weak<T>
,Weak<T>
也是一种智能指针,但是它不会增加引用计数,因此它不会阻止数据的释放。
下面是修正后的代码:
use std::rc::{Rc, Weak}; use std::cell::RefCell; struct Node { value: i32, next: RefCell<Option<Weak<Node>>>, // 注意这里使用了 Weak. } fn main() { let a = Rc::new(Node { value: 1, next: RefCell::new(None), }); let b = Rc::new(Node { value: 2, next: RefCell::new(None), }); println!("Reference count a: {}, b: {}", Rc::strong_count(&a), Rc::strong_count(&b)); // 使用Weak来避免循环引用 // 通过 downgrade 函数将 Rc 转换为 Weak。 *a.next.borrow_mut() = Some(Rc::downgrade(&b)); *b.next.borrow_mut() = Some(Rc::downgrade(&a)); println!("Value a: {}, b: {}", a.value, b.value); // Weak指针不会增加Rc的引用计数 println!("Reference count a: {}, b: {}", Rc::strong_count(&a), Rc::strong_count(&b)); } // a和b离开作用域,由于没有其他强引用,引用计数减到0,内存被释放 // 由于使用了Weak指针,即使存在循环引用,也不会导致内存泄漏
在使用值的时候,可以通过 upgrade
方法转换为 Rc<T>
并增加引用计数,保证值不会被释放。
RefCell
RefCell 实现了内部可变性, 它的值是放在堆上还是放在栈上要看值得类型.
在不使用 RefCell 的时候, 要修改一个变量的值, 你需要把这个变量设置为可变的, 但是使用 RefCell 后, 就不需要把这个变量设置为可变的, 而是在对应实例的内部修改.
use std::cell::RefCell; fn main() { // Cell 只能存储实现了 Copy 的类型, // RefCell 可以存储任何类型. let data = RefCell::new(1); { // 获得 RefCell 内部数据的可变借用 let mut v = data.borrow_mut(); *v += 1; } // 不可变引用 println!("data: {:?}", data.borrow()); }
Cell 和 RefCell 都是非线程安全的.

AtomicCell
AtomicCell 就是一个线程安全的 RefCell, 通过 crossbeam-utils 提供.
如果 AtomicCell 的泛型类型支持 CAS 那么就会使用 CSA 操作, 否则就会使用锁进行同步.
AtomicCell::<T>::is_lock_free()
返回 true, 表示 T
支持 CAS 操作.
方法和 Atomic 中的方法类似:
- new: 创建新的 AtomicCell.
- store: 保存值.
- swap: 保存值, 并返回旧值.
- compare_exchange(current, new): 如果值是 current, 就尝试将值修改为 new.
- load: 获取值.
本文作者:杂役24
本文链接:https://www.cnblogs.com/zy24/p/18458481
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步