假设我已经定义了自己的对象池结构。在内部,它保存了所有对象的Vec和一些数据结构,这些数据结构让它知道向量中的哪些项是当前分发的,哪些是免费的。它有一个分配方法,它返回向量中一个未使用的项的索引,还有一个空闲的方法,在向量中的索引处告诉池,可以再次使用。
我是否有可能定义对象池的API,使类型系统和借用检查器能够保证将对象释放回正确的池?这是假设可能有多个池实例的情况,这些实例都是相同类型的。在我看来,对于常规的全局分配程序,铁锈并不需要担心这个问题,因为只有一个全局分配程序。
用法示例:
fn foo() {
let new_obj1 = global_pool1.allocate();
let new_obj2 = global_pool2.allocate();
// do stuff with objects
global_pool1.free(new_obj2); // oops!, giving back to the wrong pool
global_pool2.free(new_obj1); // oops!, giving back to the wrong pool
}发布于 2020-08-14 07:11:59
您可以使用Zero大小的类型(简称ZST)来获得所需的API,而不需要另一个指针的开销。
以下是两个池的实现,它可以扩展为支持任意数量的池,使用宏生成“标记”结构(P1、P2等)。一个主要的缺点是,忘记使用free池会“泄漏”内存。
这个黑色金属系统博客有许多可能的改进,您可能感兴趣,特别是当您静态地分配池时,它们也有许多技巧来处理P1的可见性,这样用户就不会误用API。
use std::marker::PhantomData;
use std::{cell::RefCell, mem::size_of};
struct Index<D>(usize, PhantomData<D>);
struct Pool<D> {
data: Vec<[u8; 4]>,
free_list: RefCell<Vec<bool>>,
marker: PhantomData<D>,
}
impl<D> Pool<D> {
fn new() -> Pool<D> {
Pool {
data: vec![[0,0,0,0]],
free_list: vec![true].into(),
marker: PhantomData::default(),
}
}
fn allocate(&self) -> Index<D> {
self.free_list.borrow_mut()[0] = false;
Index(0, self.marker)
}
fn free<'a>(&self, item: Index<D>) {
self.free_list.borrow_mut()[item.0] = true;
}
}
struct P1;
fn create_pool1() -> Pool<P1> {
assert_eq!(size_of::<Index<P1>>(), size_of::<usize>());
Pool::new()
}
struct P2;
fn create_pool2() -> Pool<P2> {
Pool::new()
}
fn main() {
let global_pool1 = create_pool1();
let global_pool2 = create_pool2();
let new_obj1 = global_pool1.allocate();
let new_obj2 = global_pool2.allocate();
// do stuff with objects
global_pool1.free(new_obj1);
global_pool2.free(new_obj2);
global_pool1.free(new_obj2); // oops!, giving back to the wrong pool
global_pool2.free(new_obj1); // oops!, giving back to the wrong pool
}试图自由使用错误的池会导致:
error[E0308]: mismatched types
--> zpool\src\main.rs:57:23
|
57 | global_pool1.free(new_obj2); // oops!, giving back to the wrong pool
| ^^^^^^^^ expected struct `P1`, found struct `P2`
|
= note: expected struct `Index<P1>`
found struct `Index<P2>`这一点可以稍加改进,以便借用检查器将强制Index不会超出Pool,使用如下方法:
fn allocate(&self) -> Index<&'_ D> {
self.free_list.borrow_mut()[0] = false;
Index(0, Default::default())
}因此,如果在Index活动时删除池,则会得到此错误:
error[E0505]: cannot move out of `global_pool1` because it is borrowed
--> zpool\src\main.rs:54:10
|
49 | let new_obj1 = global_pool1.allocate();
| ------------ borrow of `global_pool1` occurs here
...
54 | drop(global_pool1);
| ^^^^^^^^^^^^ move out of `global_pool1` occurs here
...
58 | println!("{}", new_obj1.0);
| ---------- borrow later used here另外,API接口 (返回一个Item,vs only和Index)
发布于 2020-08-12 05:45:56
首先,需要考虑的是,将项插入到Vec中有时会导致它重新分配和更改地址,这意味着对Vec中项的所有现有引用都无效。我想您曾打算让用户在Vec中保留对项的引用,同时插入新项,但遗憾的是,这是不可能的。
解决这一问题的一种方法是generational_arena使用的方法。插入对象将返回一个索引。您可以调用arena.remove(index)释放对象,调用arena.get[_mut](index)获取对对象的引用,借用整个竞技场。
但是,为了论证起见,让我们假设您可以在插入新项和执行可能需要的任何其他操作时保留对竞技场的引用。考虑到引用本质上是一个指针,答案是否定的,因此无法自动记住它的来源。但是,您可以创建一个类似于Box、Rc等的“智能指针”,它保留对竞技场的引用,以便在对象被丢弃时释放它。
例如(非常粗糙的伪码):
struct Arena<T>(Vec<UnsafeCell<T>>);
struct ArenaMutPointer<'a, T> {
arena: &'a Arena,
index: usize,
}
impl<T> DerefMut for ArenaPointer<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { self.arena[self.index].get() as &mut T }
}
}
impl<T> Drop for ArenaPointer<'_, T> {
fn drop(&mut self) {
self.arena.free(self.index);
}
}发布于 2020-08-12 13:58:45
品牌塑造
人们已经多次尝试使用生命周期作为品牌,将一个特定的变量绑定到另一个变量上,而不是其他变量。
为了获得保证在范围内的索引,我们对它进行了特别的探索:在创建时检查一次,之后总是可用。
不幸的是,这需要创建不变的生命周期,以防止编译器将多个生命周期“合并”在一起,尽管可能,我还没有看到任何令人信服的API。
仿射不是线性的。
同样重要的是要注意,锈蚀没有线性型系统,而是仿射型系统。
线性型系统是指每个值精确地使用一次的系统,而仿射型系统是最多一次使用每个值的系统。
这里的结果是,很容易意外忘记将一个对象返回到池中。虽然在锈蚀中泄漏物体是安全的-- mem::forget是一种很容易做到的方法--但这些案例通常像拇指酸痛一样突出,因此相对容易被审计。另一方面,仅仅忘记将值返回池,就会导致意外泄漏,这可能会花费相当长的时间。
把它放下!
因此,解决方案是让值返回到它在Drop实现中来自的池:
当然,这是要付出代价的,即与对象一起存储的额外8个字节。
有两种可能的解决方案,在这里:
struct Thin<'a, T>(&'a Pooled<'a, T>); where struct Pooled<'a, T>(&'a Pool<T>, T);。struct Fat<'a, T>(&'a Pool<T>, &'a T);.为了简单起见,我建议从Fat替代方案开始:它更简单。
然后,Drop实现Thin或Fat只返回指向池的指针。
https://stackoverflow.com/questions/63369825
复制相似问题