2

假设我已经定义了自己的对象池结构。在内部,它保留Vec所有对象和一些数据结构,让它知道向量中的哪些项目当前已分发,哪些是免费的。它有一个 allocate 方法,该方法返回向量中未使用项的索引,以及一个 free 方法来告诉池在向量中的索引处可以再次使用。

我是否可以定义对象池的 API,以使类型系统和借用检查器保证我将对象释放回正确的池中?这是假设我可能有多个相同类型的池实例的情况。在我看来,对于常规的全局分配器,rust 不必担心这个问题,因为只有一个全局分配器。

用法示例:

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
}
4

3 回答 3

2

首先,需要考虑的是,将项目插入 aVec有时会导致它重新分配和更改地址,这意味着对项目中的所有现有引用都Vec变得无效。我想您曾打算让用户可以保留对项目的引用Vec并同时插入新项目,但遗憾的是这是不可能的。

解决此问题的一种方法是generational_arena. 插入一个对象会返回一个索引。您可以调用arena.remove(index)以释放对象,并arena.get[_mut](index)获取对该对象的引用,借用整个竞技场。

但是,为了论证,我们假设您有办法在插入新项目并执行您可能需要的任何其他操作时保持对竞技场的引用。考虑到引用本质上是一个指针,答案是否定的,没有办法自动记住它来自哪里。但是,您可以创建一个类似于 , 等的“智能指针” BoxRc它保留对竞技场的引用,以便在对象被丢弃时释放它。

例如(非常粗略的伪代码):

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-12T05:45:56.820 回答
2

品牌推广

将生命周期用作品牌的想法已经多次尝试,将特定变量与另一个变量联系起来而不是其他变量。

为了获得保证在范围内的索引,已经特别探索了它:在创建时检查一次,之后始终可用。

不幸的是,这需要创建不变的生命周期以防止编译器将多个生命周期“合并”在一起,尽管可能我还没有看到任何引人注目的 API。

仿射,而不是线性。

同样重要的是要注意,Rust 没有线性类型系统,而是仿射类型系统。

线性类型系统是每个值仅使用一次的系统,而仿射类型系统是每个值最多使用一次的系统。

在这里,结果是很容易不小心忘记将对象返回到池中。虽然在 Rust 中泄漏对象总是安全的——而且mem::forget是一种简单的方法——但这些情况通常很突出,因此相对容易审核。另一方面,仅仅忘记将值返回到池中会导致意外泄漏,这可能会花费相当长的时间。

放下它!

因此,解决方案是让值在其实现中返回到它来自的池中Drop

  • 它使您不可能忘记意外返回。
  • 它要求对象持有对它来自的池的引用,从而不可能意外地将其返回到错误的池中。

当然,它确实是有代价的,即与对象一起存储了额外的 8 个字节。

这里有两种可能的解决方案:

  • 细指针:struct Thin<'a, T>(&'a Pooled<'a, T>);在哪里struct Pooled<'a, T>(&'a Pool<T>, T);
  • 胖子指点:struct Fat<'a, T>(&'a Pool<T>, &'a T);

为简单起见,我建议从Fat替代方案开始:它更简单。

然后Drop执行Thin或者Fat只是返回指向池的指针。

于 2020-08-12T13:58:45.400 回答
2

您可以使用零大小类型(简称 ZST)来获取您想要的 API,而无需另一个指针的开销。

这是 2 个池的实现,可以扩展为支持任意数量的池,使用宏生成“标记”结构(P1、、P2等)。一个主要的缺点是忘记free使用池会“泄漏”内存。

这篇Ferrous Systems 博客文章有许多您可能感兴趣的改进,尤其是在您静态分配池的情况下,并且它们还有许多技巧可以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())
}

因此,如果在 anIndex还活着的情况下删除了池,则会出现此错误:

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

链接到游乐场

此外,带有ItemAPI的游乐场链接(仅返回Item, vs 和Index

于 2020-08-14T07:11:59.440 回答