Rust 中的智能指针:是什么、为什么以及如何?
TL;DR:我将介绍一些 Rust 的智能指针:
Box
,,,,,和Cell
。RefCell
Rc
Arc
RwLock
Mutex
智能指针显然是……智能的指针。但这个“智能”究竟是什么意思呢?什么时候应该使用它们?它们是如何工作的?
这些就是我将在这里开始解答的问题。这只是一个答案的开端,仅此而已。我希望本文能为您提供“理解背景”(例如对概念的“熟悉”),帮助您真正理解这个主题,而这需要阅读官方文档,当然,也需要实践。
如果你已经熟悉了,可以把这篇文章作为相关阅读材料的清单。请查看每节开头的“有用链接”。
指数:
智能点一般
正如《Rust 之书》中所解释的,指针是包含“指向”其他数据的地址的变量。Rust 中常见的指针是引用 ( &
)。智能指针是“具有额外元数据和功能”的指针,例如,它们可以计算值被借用的次数,提供管理读写锁的方法等等。
从技术上讲,String
和Vec
也是智能指针,但我不会在这里介绍它们,因为它们很常见并且通常被认为是类型而不是指针。
还要注意,在此列表中,只有Arc
、RwLock
和Mutex
是线程安全的。
Box<T>
什么?
Box<T>
允许你存储T
在堆上。所以,如果你有一个本来u64
应该存储在栈上的变量,Box<u64>
它会被存储在堆上。
如果您对堆栈和堆的概念不太熟悉,请阅读此文。
为什么?
存储在栈中的值不能增长,因为 Rust 需要在编译时知道它的大小。据我所知,关于这会如何影响你的编程的最好例子是《递归》。请考虑下面的代码(及其注释)。
// This does not compile. The List contains itself,
// being recursive and therefore having an infinite size.
enum List {
Cons(i32, List),
Nil,
}
// This does compile because the size of a pointer
// does not change according to the size of the pointed value.
enum List {
Cons(i32, Box<List>),
Nil,
}
更一般地说,Box
当你的值太大而无法保存在堆栈中或者你需要拥有它时,它很有用。
如何?
T
要获取其中的值,Box<T>
你只需取消引用它。
let boxed = Box::new(11);
assert_eq!(*boxed, 11)
单元格<T>
什么?
Cell<T>
提供对 的共享引用,T
同时允许您更改T
。这是 模块提供的“可共享可变容器”之一std::cell
。
为什么?
在 Rust 中,共享引用是不可变的。这保证了当你访问内部值时,不会得到与预期不同的结果,同时也确保了你不会在它被释放后尝试访问它(这在 70% 的内存安全漏洞中占了很大一部分)。
如何?
它Cell<T>
提供了控制我们对 的访问的函数。你可以在这里T
找到它们,但为了便于解释,我们只需要两个:和。get()
set()
基本上,Cell<T>
允许你自由地使用 进行修改,T
因为T.set()
当你使用 时T.get()
,你获取Copy
的是 的T
,而不是引用。这样,即使你更改T
,你使用 获得的复制值get()
也将保持不变;如果你销毁T
,也不会有任何指针悬空。
最后要注意的是,这也T
必须实现。Copy
use std::cell::Cell;
let container = Cell::new(11);
let eleven = container.get();
{
container.set(12);
}
let twelve = container.get();
assert_eq!(eleven, 11);
assert_eq!(twelve, 12);
RefCell<T>
什么?
RefCell<T>
也给出了对的共享引用T
,但是虽然Cell
是静态检查的(Rust 在编译时检查它),但却RefCell<T>
是动态检查的(Rust 在运行时检查它)。
为什么?
因为Cell
使用副本进行操作,所以您应该限制自己使用较小的值,这意味着您再次需要引用,这会让我们回到Cell
解决的问题。
处理这个问题的方法RefCell
是跟踪谁在读,谁在写T
。这就是为什么RefCell<T>
要动态检查:因为你要编写这个检查代码。不过不用担心,Rust 仍然会确保你在编译时不会搞砸。
如何?
RefCell<T>
有一些方法可以借用 的可变或不可变引用T
;如果操作不安全,则不允许你这样做。与 一样Cell
, 中也有一些方法RefCell
,但这两个方法足以说明这个概念:borrow()
,它获取一个不可变引用;borrow_mut()
,它获取一个可变引用。 使用的逻辑RefCell
大致如下:
- 如果没有对的引用(无论是可变的还是不可变的)
T
,您可能会获得对它的可变或不可变的引用; - 如果已经有一个对的可变引用
T
,则您可能什么也得不到,而必须等到该引用被删除; - 如果有一个或多个对的不可变引用
T
,您可能会获得对它的不可变引用。
正如您所见,没有办法T
同时获取可变和不可变引用。
记住:这不是线程安全的。我说的“不可能”指的是单线程。
另一种思考方式是:
- 不可变引用是共享引用;
- 可变引用是唯一引用。
值得一提的是,上面提到的函数有一些变体不会惊慌,而是返回Result
:try_borrow()
和try_borrow_mut()
;
use std::cell::RefCell;
let container = RefCell::new(11);
{
let _c = container.borrow();
// You may borrow as immutable as many times as you want,...
assert!(container.try_borrow().is_ok());
// ...but cannot borrow as mutable because
// it is already borrowed as immutable.
assert!(container.try_borrow_mut().is_err());
}
// After the first borrow as mutable...
let _c = container.borrow_mut();
// ...you cannot borrow in any way.
assert!(container.try_borrow().is_err());
assert!(container.try_borrow_mut().is_err());
Rc<T>
什么?
我将引用此文档:
该类型提供对堆上分配的
Rc<T>
类型为 的值的共享所有权。调用on会产生一个指向堆上相同分配空间的新指针。当指向给定分配空间的最后一个指针被销毁时,存储在该分配空间中的值(通常称为“内部值”)也会被丢弃。T
clone
Rc
Rc
因此,就像 一样Box<T>
,在堆上Rc<T>
分配。区别在于,克隆会在另一个 内部生成另一个,而克隆会将另一个 克隆到同一个。T
Box<T>
T
Box
Rc<T>
Rc
T
另一个重要的评论是,我们在 中没有Rc
像在Cell
或 中那样的内部可变性RefCell
。
为什么?
您希望对某些值进行共享访问(而不复制它),但您希望在不再使用它时(即当没有对它的引用时)释放它。
由于 没有内部可变性Rc
,因此通常将其与Cell
或一起使用RefCell
,例如Rc<Cell<T>>
。
如何?
使用Rc<T>
,您正在使用clone()
方法。在后台,它会计算您拥有的引用数,当它变为零时,它会减少T
。
use std::rc::Rc;
let mut c = Rc::new(11);
{
// After borrwing as immutable...
let _first = c.clone();
// ...you can no longer borrow as mutable,...
assert_eq!(Rc::get_mut(&mut c), None);
// ...but can still borrow as immutable.
let _second = c.clone();
// Here we have 3 pointer ("c", "_first" and "_second").
assert_eq!(Rc::strong_count(&c), 3);
}
// After we drop the last two, we remain only with "c" itself.
assert_eq!(Rc::strong_count(&c), 1);
// And now we can borrow it as mutable.
let z = Rc::get_mut(&mut c).unwrap();
*z += 1;
assert_eq!(*c, 12);
Arc<T>
什么?
Arc
是的线程安全版本Rc
,因为它的计数器是通过原子操作进行管理的。
为什么?
我认为使用Arc
而不是 的原因Rc
很明显(线程安全),所以相关的问题变成了:为什么不Arc
每次都使用?答案是 提供的这些额外控制Arc
会带来开销。
如何?
就像Rc
,Arc<T>
您将使用clone()
来获取指向相同值的指针T
,一旦最后一个指针被删除,该指针就会被销毁。
use std::sync::Arc;
use std::thread;
let val = Arc::new(0);
for i in 0..10 {
let val = Arc::clone(&val);
// You could not do this with "Rc"
thread::spawn(move || {
println!(
"Value: {:?} / Active pointers: {}",
*val+i,
Arc::strong_count(&val)
);
});
}
RwLock<T>
有用的链接:文档。
RwLock
也由板条箱提供parking_lot
。
什么?
作为读写锁,只有持有以下锁之一时RwLock<T>
才会授予访问权限:或,遵循以下规则:T
read
write
- 读:如果你想要一个读锁,只要没有写者持有该锁,你就可以得到它;否则,你必须等到它被释放;
- 写入:如果您想要一个写入锁,只要没有人(读者或写入者)持有锁,您就可以获得该锁;否则,您必须等到他们被放弃;
为什么?
RwLock
允许你从多个线程读写相同的数据。与Mutex
(见下文)不同,它区分锁的类型,因此read
,只要你没有write
锁,就可以拥有多个锁。
如何?
当你想要读取 a 时RwLock
,你必须使用函数read()
—or try_read()
—,它会返回一个LockResult
包含 a 的 a RwLockReadGuard
。如果读取成功,你就可以RwLockReadGuard
通过 deref 访问其中的值。如果写入者持有锁,则线程将被阻塞,直到它能够获取锁为止。
当你尝试使用write()
—or时也会发生类似的情况try_write()
。不同之处在于,它不仅会等待持有锁的写入者,还会等待任何持有锁的读取者。
use std::sync::RwLock;
let lock = RwLock::new(11);
{
let _r1 = lock.read().unwrap();
// You may pile as many read locks as you want.
assert!(lock.try_read().is_ok());
// But you cannot write.
assert!(lock.try_write().is_err());
// Note that if you use "write()" instead of "try_write()"
// it will wait until all the other locks are released
// (in this case, never).
}
// If you grab the write lock, you may easily change it
let mut l = lock.write().unwrap();
*l += 1;
assert_eq!(*l, 12);
如果某个持有锁的线程崩溃了,进一步尝试获取锁将返回PoisonError
,这意味着从那时起每次尝试读取RwLock
都将返回相同的。您可以使用PoisonError
来从中毒中恢复。RwLock
into_inner()
use std::sync::{Arc, RwLock};
use std::thread;
let lock = Arc::new(RwLock::new(11));
let c_lock = Arc::clone(&lock);
let _ = thread::spawn(move || {
let _lock = c_lock.write().unwrap();
panic!(); // the lock gets poisoned
}).join();
let read = match lock.read(){
Ok(l) => *l,
Err(poisoned) => {
let r = poisoned.into_inner();
*r + 1
}
};
// It will be 12 because it was recovered from the poisoned lock
assert_eq!(read,12);
互斥锁
Mutex
也由板条箱提供parking_lot
。
什么?
Mutex
与 类似RwLock
,但它只允许一个锁持有者,无论它是读者还是作者。
为什么?
Mutex
优于的一个原因RwLock
是RwLock
可能会导致写入器饥饿(当读者蜂拥而至并且写入器永远没有机会获得锁时,永远等待),而 则不会发生这种情况Mutex
。
当然,我们在这里深入探讨,因此现实生活中的选择取决于更高级的考虑,例如您期望同时有多少个读者,操作系统如何实现锁,等等......
如何?
Mutex
和 的RwLock
工作方式类似,不同之处在于,由于Mutex
不区分读者和作者,因此您只需使用lock()
或try_lock
即可获取MutexGuard
。 中毒逻辑也发生在这里。
use std::sync::Mutex;
let guard = Mutex::new(11);
let mut lock = guard.lock().unwrap();
// It does not matter if you are locking the Mutex to read or write,
// you can only lock it once.
assert!(guard.try_lock().is_err());
// You may change it just like you did with RwLock
*lock += 1;
assert_eq!(*lock, 12);
您可以采用与Mutex
处理中毒相同的方法来处理中毒RwLock
。
感谢您的阅读!
鏂囩珷鏉ユ簮锛�https://dev.to/rogertorres/smart-pointers-in-rust-what-why-and-how-oma