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来返回一个&mutcell 引用。所以这类似于两边的借用。我们允许对 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