6个版本
0.5.4 | 2023年7月13日 |
---|---|
0.5.3 | 2023年1月15日 |
0.5.2 | 2022年6月11日 |
0.5.0 | 2022年1月23日 |
0.2.0 |
|
#40 in 内存管理
12,075 每月下载量
用于 13 个crates(6个直接)
175KB
1.5K SLoC
RefCell或RwLock的静态检查替代方案
Cell类型,与RefCell不同,在运行时不会引发panic,而是会在编译时给出编译错误,或者用粗粒度的锁定(RwLock)代替细粒度的锁定。
文档
请参阅crates文档。
许可证
本项目采用Apache License 2.0或MIT许可证,由您选择。(见LICENSE-APACHE和LICENSE-MIT)。
贡献
除非您明确表示,否则任何有意提交以包含在本crate中的贡献,根据Apache-2.0许可证定义,均应以上述方式双重许可,不得附加任何额外条款或条件。
lib.rs
:
RefCell和RwLock的静态检查替代方案。
此crate提供了四种RefCell的替代方案,每个方案都在编译时(静态)检查从cell的借用,而不是像RefCell那样在运行时检查。检查机制对所有四种方案都相同。它们只在表示所有权的方式上有所不同:QCell使用整数ID,TCell和TLCell使用标记类型,而LCell使用Rust生命周期。每种方法都有其优点和缺点。
以 QCell
为例:QCell
是一种单元格类型,其中单元格内容在借用目的上逻辑上由所有者类型的实例“拥有”,QCellOwner
。因此,只能通过在该所有者上执行借用调用来访问单元格内容。这类似于从结构中借用字段或从 Vec
中借用元素。但实际上,对象之间的唯一联系是在创建单元格时提供了所有者实例的引用。实际上,借用所有者和释放所有者是分离的。
这使编译器能够在编译时静态检查对存储在 Rc
引用(或其他引用类型)背后的数据的可变访问。这种模式如下:所有者保持在栈上,并将对其的可变引用传递到栈上的调用中(例如作为上下文结构的一部分)。这通过借用检查器在编译时完全检查。然后,通过在所有者实例上使用借用调用以访问单元格内容,将这种静态借用检查扩展到单元格内容(在 Rc
后面)。这提供了一个编译时保证,即对单元格内容的访问是安全的。
另一种选择是使用 RefCell
,如果尝试对同一数据执行两次可变引用,它将引发恐慌。使用 RefCell
时,没有警告或错误可以在编译时检测到问题。另一方面,使用 QCell
时,错误将在编译时检测到,但限制比实际需要严格得多。例如,如果它们由同一个所有者保护,则无法同时从超过几个不同的单元格中借用(RefCell
会正确地允许)。然而,如果您能够在这些限制内工作(例如,通过仅保留短时间的借用),那么优点是永远不会因为错误的借用使用而引发恐慌,因为一切都是由编译器检查的。
除了 QCell
和 QCellOwner
之外,此软件包还提供了 TCell
和 TCellOwner
,它们的工作方式相同,但使用标记类型而不是所有者 ID,TLCell
和 TLCellOwner
也使用标记类型,但它们是线程局部的,以及 LCell
和 LCellOwner
使用生命周期。请参阅下面的 "单元格类型比较"。
示例
使用 RefCell
,这可以编译但会引发恐慌
let item = Rc::new(RefCell::new(Vec::<u8>::new()));
let mut iref = item.borrow_mut();
test(&item);
iref.push(1);
fn test(item: &Rc<RefCell<Vec<u8>>>) {
item.borrow_mut().push(2); // Panics here
}
使用 QCell
,它拒绝编译
let mut owner = QCellOwner::new();
let item = Rc::new(QCell::new(&owner, Vec::<u8>::new()));
let iref = owner.rw(&item);
test(&mut owner, &item); // Compile error
iref.push(1);
fn test(owner: &mut QCellOwner, item: &Rc<QCell<Vec<u8>>>) {
owner.rw(&item).push(2);
}
在这两种情况下,解决方案都是确保在调用时 iref
不是活动的,但 QCell
使用标准编译时借用检查来强制修复错误。这是使用这些类型的主要优势。
以下是使用 TCell
的有效版本
再次使用 LCell
LCellOwner::scope(|mut owner| {
let item = Rc::new(LCell::new(Vec::<u8>::new()));
let iref = owner.rw(&item);
iref.push(1);
test(&mut owner, &item);
});
fn test<'id>(owner: &mut LCellOwner<'id>, item: &Rc<LCell<'id, Vec<u8>>>) {
owner.rw(&item).push(2);
}
为什么这是安全的
这是声明此 crate 接口安全的理由
-
在创建和销毁 cell 之间,唯一访问内容(读取或写入)的方式是通过借用所有者实例。因此,借用所有者是这些数据的独家守护者。
-
借用调用需要一个
&
所有者引用来返回一个&
cell 引用,或者对所有者的&mut
来返回一个&mut
cell 引用。所以这类似于两边的借用。我们允许对 cell 的唯一借用是 Rust 允许对借用所有者的借用,并且当这种借用活动时,借用所有者和 cell 的引用被阻止进行进一步的互斥借用。cell 的内容就像是由借用所有者拥有,就像Vec
内的元素一样。所以 Rust 的保证得到了维护。 -
借用所有者无法控制 cell 内容何时被丢弃,因此在此点不能作为数据的门卫。然而,这不会与数据的任何活动借用冲突,因为在借用活动期间,Rust 的借用检查会锁定 cell 的引用。如果这后面是
Rc
,则直到该借用释放之前,不可能释放最后一个强引用。
如果您在这个推理或代码中看到任何缺陷,请提出问题,最好是带有演示问题的测试代码。Rust playground 中的 MIRI 可以报告一些不安全的情况。
cell 类型比较
RefCell
优点和缺点
- 优点:简单
- 优点:允许非常灵活的借用模式
- 优点:支持
no_std
- 缺点:没有编译时借用检查
- 缺点:可能因远程代码更改而引发恐慌
- 缺点:运行时借用检查和某些 cell 空间开销
QCell
优点和缺点
- 优点:简单
- 优点:编译时借用检查
- 优点:动态所有者创建,不受任何限制
- 优点:不需要寿命注释或类型参数
- 优点:支持
no_std
- 缺点:一次只能借用最多 3 个对象
- 缺点:运行时所有者检查和某些 cell 空间开销
- 优点:编译时借用检查
- 优点:借用或所有权检查的运行时开销为零
- 优点:没有 cell 空间开销
- 优点:支持
no_std
(通过外部 crate) - 缺点:一次只能借用最多 3 个对象
- 缺点:使用单例,要么是按进程(TCell)要么是按线程(TLCell),这意味着每个线程或进程的标记类型只允许一个所有者。旨在在调用栈上嵌套的代码必须用外部标记类型参数化。
LCell
优点和缺点
- 优点:编译时借用检查
- 优点:借用或所有权检查的运行时开销为零
- 优点:没有 cell 空间开销
- 优点:不需要单例,这意味着一次使用不会限制其他嵌套使用
- 优点:支持
no_std
- 缺点:一次只能借用最多 3 个对象
- 缺点:需要在调用和结构中添加寿命注释
Cell | 所有者 ID | Cell 开销 | 借阅检查 | 所有者检查 | 创建所有者检查 |
---|---|---|---|---|---|
RefCell |
不适用 | usize |
运行时 | 不适用 | 不适用 |
QCell |
整数 | usize |
编译时 | 运行时 | 运行时 |
TCell 或 TLCell |
标记类型 | 无 | 编译时 | 编译时 | 运行时 |
LCell |
生命周期 | 无 | 编译时 | 编译时 | 编译时 |
所有者易用性
Cell | 所有者类型 | 创建所有者 |
---|---|---|
RefCell |
不适用 | 不适用 |
QCell |
QCellOwner |
QCellOwner::新() |
TCell 或TLCell |
ACellOwner (或 BCellOwner 或 CCellOwner 等) |
结构体 MarkerA; 类型 ACell<T> = TCell<MarkerA, T>; 类型 ACellOwner = TCellOwner<MarkerA>; ACellOwner::新() |
LCell |
LCellOwner<'id> |
LCellOwner::scope( |owner | { ... }) |
单元易用性
Cell | 单元类型 | 单元创建 |
---|---|---|
RefCell |
RefCell<T> |
RefCell::新(v) |
QCell |
QCell<T> |
owner.cell(v) 或 QCell::new(&owner, v) |
TCell 或 TLCell |
ACell<T> |
owner.cell(v) 或 ACell::new(v) |
LCell |
LCell<'id, T> |
owner.cell(v) 或 LCell::new(v) |
借用易用性
Cell | 单元不可变借用 | 单元可变借用 |
---|---|---|
RefCell |
单元.借用() |
单元.借用_mut() |
QCell |
cell.ro(&owner) 或所有者.ro(&单元) |
cell.rw(&mut owner) 或所有者.rw(&单元) |
TCell 或 TLCell |
cell.ro(&owner) 或所有者.ro(&单元) |
cell.rw(&mut owner) 或所有者.rw(&单元) |
LCell |
cell.ro(&owner) 或所有者.ro(&单元) |
cell.rw(&mut owner) 或所有者.rw(&单元) |
多线程使用:Send 和 Sync
通常,单元格所有者只由一个线程持有,所有对单元格的访问都在该线程中进行。然而,在某些情况下,如果包含的类型允许,仍然可以在线程之间传递或共享这些对象。
Cell | 所有者类型 | 单元类型 |
---|---|---|
RefCell |
不适用 | Send |
QCell |
Send + Sync | Send + Sync |
TCell |
Send + Sync | Send + Sync |
TLCell |
Send | |
LCell |
Send + Sync | Send + Sync |
感谢 Github 用户 Migi 和 pythonesque 对启用 Send 和/或 Sync 的理由进行论证。(pythonesque 的 GhostCell
是一个基于生命周期的单元,它早于 LCell
,但直到 2021 年才正式发表。该论文的作者证明了 GhostCell
的逻辑推理是正确的,这间接加强了类似单元类型(如本软件包中的类型)的理论论证。)
以下是推理概述
-
与
RefCell
不同,这些单元格类型可能是Sync
,因为可变访问受到单元格所有者的保护。只有当您对单元格所有者具有可变访问权限时,才能获取单元格内容的可变访问权限。(请注意,Sync
仅在包含类型是Send + Sync
时才可用。) -
单元格所有者可能是
Sync
,因为Sync
只允许线程间对单元格所有者的共享不可变访问。因此,在两个线程中可能存在&QCell
和&QCellOwner
引用,但只能像那样进行单元格内容的不可变访问,因此不存在安全性问题。 -
一般来说,
Send
是安全的,因为这表示从一个线程到另一个线程的某些权利的完全转移(假设包含的类型也是Send
)。 -
TLCell
是一个例外,因为在每个线程中可以有不同的所有者,其标记类型相同,因此所有者不能被发送或共享。此外,如果两个线程对同一单元格有&TLCell
引用,则可以在两个线程中创建包含数据的可变引用,这会破坏 Rust 的保证。因此,TLCell
不能是Sync
。然而,它可以是Send
,因为在那种情况下,访问数据的权利被完全从一个线程转移到另一个线程。
多线程使用:RwLock
QCell
和类似类型也可以用作 RwLock
的替代品。例如,如果您有一个 Arc<RwLock<T>>
的集合,您可以将它们替换为 Arc<QCell<T>>
。本质上,您是将针对每个单个 T
的细粒度锁定(一个用于每个单个 T
)交换为围绕 QCellOwner
的粗粒度锁定。根据访问模式,这可能会好或更差。例如,如果您经常需要在一次逻辑操作中访问多个 T
实例,并且大锁的竞争很小,那么这将更好。或者,如果您已经对包含 QCellOwner
的 struct
有 &mut
,那么您几乎可以免费获得对 T
实例的访问。
no_std
支持
qcell crate 可以在四个级别上进行构建
-
完整的
std
支持,这是默认值 -
no_std
与 exclusion-set 一起,当使用--no-default-features
和--features exclusion-set
构建 -
no_std
与alloc
一起,当使用--no-default-features
和--features alloc
构建 -
no_std
无alloc
,在构建时使用--no-default-features
两者 QCell
和 LCell
都支持所有四个级别,而 TCell
也适用于前两个。
命名来源
"Q"最初指的是量子纠缠,这个想法是这是一种远程所有权的概念。"T"指的是它基于类型系统,"TL"是线程局部,"L"是基于生命周期。
阻止不安全代码模式
请参阅doctest_qcell
、doctest_tcell
、doctest_tlcell
、doctest_lcell
、doctest_qcell_noalloc
和 doctest_lcell_generativity
模块,其文档和 doc-tests 展示了哪些不安全模式被阻止。
依赖关系
~0-26MB
~329K SLoC