【问题标题】:Lifetime constraints to model scoped garbage collection对作用域垃圾收集建模的生命周期约束
【发布时间】:2018-08-17 09:38:22
【问题描述】:

我正在与一位朋友合作,为“作用域”垃圾收集器的生命周期定义一个安全的公共 API。生命周期要么受到过度约束,正确的代码无法编译,要么生命周期太松,它们可能允许无效行为。在尝试了多种方法之后,我们仍然无法获得正确的 API。这尤其令人沮丧,因为 Rust 的生命周期可以帮助避免这种情况下的错误,但现在它看起来很顽固。

作用域垃圾回收

我正在实现一个 ActionScript 解释器并且需要一个垃圾收集器。我研究了rust-gc,但它不适合我的需要。主要原因是它要求垃圾收集的值具有a static lifetime,因为 GC 状态是线程局部静态变量。我需要将垃圾收集绑定到动态创建的主机对象。避免使用全局变量的另一个原因是我更容易处理多个独立的垃圾收集范围、控制它们的内存限制或序列化它们。

作用域垃圾收集器类似于typed-arena。您可以使用它来分配值,并且一旦丢弃垃圾收集器,它们就会全部释放。不同的是,你还可以在其生命周期内触发垃圾回收,它会清理无法访问的数据(并且不限于单一类型)。

我有a working implementation implemented (mark & sweep GC with scopes),但界面还不能安全使用。

这是我想要的用法示例:

pub struct RefNamedObject<'a> {
    pub name: &'a str,
    pub other: Option<Gc<'a, GcRefCell<NamedObject<'a>>>>,
}

fn main() {
    // Initialize host settings: in our case the host object will be replaced by a string
    // In this case it lives for the duration of `main`
    let host = String::from("HostConfig");

    {
        // Create the garbage-collected scope (similar usage to `TypedArena`)
        let gc_scope = GcScope::new();

        // Allocate a garbage-collected string: returns a smart pointer `Gc` for this data
        let a: Gc<String> = gc_scope.alloc(String::from("a")).unwrap();

        {
            let b = gc_scope.alloc(String::from("b")).unwrap();
        }

        // Manually trigger garbage collection: will free b's memory
        gc_scope.collect_garbage();

        // Allocate data and get a Gc pointer, data references `host`
        let host_binding: Gc<RefNamed> = gc_scope
            .alloc(RefNamedObject {
                name: &host,
                other: None,
            })
            .unwrap();

        // At the end of this block, gc_scope is dropped with all its
        // remaining values (`a` and `host_bindings`)
    }
}

生命周期属性

基本直觉是Gc 只能包含与对应的GcScope 一样长(或更长)的数据。 Gc 类似于 Rc 但支持循环。您需要使用Gc&lt;GcRefCell&lt;T&gt;&gt; 来改变值(类似于Rc&lt;RefCell&lt;T&gt;&gt;)。

以下是我的 API 的生命周期必须满足的属性:

Gc 的寿命不能超过它的 GcScope

以下代码必须失败,因为agc_scope 更有效:

let a: Gc<String>;
{
    let gc_scope = GcScope::new();
    a = gc_scope.alloc(String::from("a")).unwrap();
}
// This must fail: the gc_scope was dropped with all its values
println("{}", *a); // Invalid

Gc 不能包含比 GcScope 更短的数据

以下代码必须失败,因为msg 的寿命不如gc_scope 长(或更长)

let gc_scope = GcScope::new();
let a: Gc<&string>;
{
    let msg = String::from("msg");
    a = gc.alloc(&msg).unwrap();
}

必须可以分配多个Gcgc_scope 上不排除)

以下代码必须编译

let gc_scope = GcScope::new();

let a = gc_scope.alloc(String::from("a"));
let b = gc_scope.alloc(String::from("b"));

必须可以分配包含生命周期长于gc_scope 的引用的值

以下代码必须编译

let msg = String::from("msg");
let gc_scope = GcScope::new();
let a: Gc<&str> = gc_scope.alloc(&msg).unwrap();

必须可以创建 Gc 指针的循环(这就是重点)

Rc&lt;Refcell&lt;T&gt;&gt; 模式类似,您可以使用Gc&lt;GcRefCell&lt;T&gt;&gt; 来改变值并创建循环:

// The lifetimes correspond to my best solution so far, they can change
struct CircularObj<'a> {
    pub other: Option<Gc<'a, GcRefCell<CircularObj<'a>>>>,
}

let gc_scope = GcScope::new();

let n1 = gc_scope.alloc(GcRefCell::new(CircularObj { other: None }));
let n2 = gc_scope.alloc(GcRefCell::new(CircularObj {
    other: Some(Gc::clone(&n1)),
}));
n1.borrow_mut().other = Some(Gc::clone(&n2));

目前的解决方案

自动生命周期/生命周期标记

auto-lifetime branch上实现

此解决方案的灵感来自neon 的句柄。 这允许任何有效的代码编译(并允许我测试我的实现)但是太松并且允许无效代码。 它允许Gc 比创建它的gc_scope 寿命更长。 (违反第一个属性)

这里的想法是我将一个生命周期 'gc 添加到我的所有结构中。这个想法是这个生命周期代表“gc_scope 的生命周期”。

// A smart pointer for `T` valid during `'gc`
pub struct Gc<'gc, T: Trace + 'gc> {
    pub ptr: NonNull<GcBox<T>>,
    pub phantom: PhantomData<&'gc T>,
    pub rooted: Cell<bool>,
}

我称之为自动生命周期,因为这些方法从不将这些结构生命周期与它们接收的引用的生命周期混合。

这是 gc_scope.alloc 的实现:

impl<'gc> GcScope<'gc> {
    // ...
    pub fn alloc<T: Trace + 'gc>(&self, value: T) -> Result<Gc<'gc, T>, GcAllocErr> {
        // ...
    }
}

内部/外部生命周期

inner-outer branch上实现

此实现尝试通过将GcGcScope 的生命周期相关联来解决上一个问题。 它被过度约束并阻止了循环的创建。这违反了最后一个属性。

为了约束Gc 相对于它的GcScope,我引入了两个生命周期:'innerGcScope 的生命周期,结果是Gc&lt;'inner, T&gt;'outer 表示比 'inner 更长的生命周期,用于分配的值。

这是分配签名:

impl<'outer> GcScope<'outer> {
    // ...

    pub fn alloc<'inner, T: Trace + 'outer>(
        &'inner self,
        value: T,
    ) -> Result<Gc<'inner, T>, GcAllocErr> {
        // ...
    }

    // ...
}

闭包(上下文管理)

with branch上实现

另一个想法是不要让用户使用GcScope::new 手动创建GcScope,而是公开一个函数GcScope::with(executor),提供对gc_scope 的引用。闭包executor 对应于gc_scope。到目前为止,它要么阻止使用外部引用,要么允许将数据泄漏到外部Gc 变量(第一个和第四个属性)。

这是分配签名:

impl<'gc> GcScope<'gc> {
    // ...
    pub fn alloc<T: Trace + 'gc>(&self, value: T) -> Result<Gc<'gc, T>, GcAllocErr> {
        // ...
    }
}

这是一个显示违反第一个属性的用法示例:

let message = GcScope::with(|scope| {
    scope
        .alloc(NamedObject {
            name: String::from("Hello, World!"),
        })
        .unwrap()
});
println!("{}", message.name);

我想要什么

据我了解,我想要的alloc 签名是:

impl<'gc> GcScope<'gc> {
    pub fn alloc<T: Trace + 'gc>(&'gc self, value: T) -> Result<Gc<'gc, T>, GcAllocErr> {
        // ...
    }
}

一切都比selfgc_scope)活得更长或更久。但这会因最简单的测试而失败:

fn test_gc() {
    let scope: GcScope = GcScope::new();
    scope.alloc(String::from("Hello, World!")).unwrap();
}

原因

error[E0597]: `scope` does not live long enough
  --> src/test.rs:50:3
   |
50 |   scope.alloc(String::from("Hello, World!")).unwrap();
   |   ^^^^^ borrowed value does not live long enough
51 | }
   | - `scope` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

我不知道这里会发生什么。 Playground link

编辑:正如在 IRC 上向我解释的那样,这是因为我实现了需要 &amp;mut selfDrop,但 scope 已经以只读模式借用。

概述

这里是我的库的主要组件的快速概览。 GcScope 包含一个 RefCell 到其可变状态。这被引入不需要&amp;mut self 用于alloc,因为它“锁定”了 gc_scope 并违反了属性 3:分配多个值。 这个可变状态是GcState。它跟踪所有分配的值。这些值存储为GcBox 的只进链表。这个GcBox 是堆分配的并且包含带有一些元数据的实际值(有多少活动的Gc 指针将它作为它们的根和一个用于检查该值是否可以从根访问的布尔标志(参见rust-gc) . 这里的值必须超过它的gc_scope 所以GcBox 使用一个生命周期,然后GcState 必须使用一个生命周期以及GcScope:这总是相同的生命周期,意思是“比gc_scope 更长” .GcScope 具有RefCell(内部可变性)和生命周期这一事实可能是我无法让生命周期工作的原因(它会导致不变性?)。

Gc 是指向某些gc_scope 分配数据的智能指针。您只能通过gc_scope.alloc 或克隆它来获取它。 GcRefCell 很可能没问题,它只是一个 RefCell 包装器,添加了元数据和行为以正确支持借用。

灵活性

我可以满足以下要求以获得解决方案:

  • 不安全的代码
  • 夜间功能
  • API 更改(参见例如我的with 方法)。重要的是我可以创建一个临时区域,我可以在其中操作垃圾收集的值,并且在此之后将它们全部丢弃。这些垃圾收集的值需要能够访问范围之外的寿命更长(但不是静态)的变量。

The repositoryscoped-gc/src/lib.rs(编译失败)中有一些测试为scoped-gc/src/test.rs

我找到了解决方案,我会在编辑后发布。

【问题讨论】:

    标签: garbage-collection rust lifetime


    【解决方案1】:

    这是迄今为止我在使用 Rust 时遇到的最困难的问题之一,但我设法找到了解决方案。感谢 panicbit 和 mbrubeck 在 IRC 上帮助我。

    帮助我前进的是对我在问题末尾发布的错误的解释:

    error[E0597]: `scope` does not live long enough
      --> src/test.rs:50:3
       |
    50 |   scope.alloc(String::from("Hello, World!")).unwrap();
       |   ^^^^^ borrowed value does not live long enough
    51 | }
       | - `scope` dropped here while still borrowed
       |
       = note: values in a scope are dropped in the opposite order they are created
    

    我不明白这个错误,因为我不清楚为什么scope 被借用,多长时间,或者为什么在范围结束时不再需要借用。

    原因是在分配值期间,scope 在分配值的持续时间内被不可变地借用。现在的问题是作用域包含一个实现“Drop”的状态对象:drop 的自定义实现使用&amp;mut self -> 无法为 drop 获得可变借用,而价值已经被一成不变地借来了。

    了解 drop 需要 &amp;mut self 并且它与不可变借用不兼容,从而解锁了这种情况。

    事实证明,上述问题中描述的内部-外部方法与alloc 具有正确的生命周期:

    impl<'outer> GcScope<'outer> {
        // ...
    
        pub fn alloc<'inner, T: Trace + 'outer>(
            &'inner self,
            value: T,
        ) -> Result<Gc<'inner, T>, GcAllocErr> {
            // ...
        }
    
        // ...
    }
    

    返回的Gc 的寿命与GcScope 一样长,并且分配的值必须比当前的GcScope 寿命更长。如问题中所述,此解决方案的问题在于它不支持循环值。

    循环值不起作用不是因为alloc 的生命周期,而是因为自定义drop。删除 drop 允许所有测试通过(但内存泄漏)。

    解释很有意思:

    alloc 的生命周期表示分配值的属性。分配的值不能超过它们的GcScope,但它们的内容必须与GcScope 一样长或更长。在创建循环时,该值受这两个约束:它已分配,因此必须与GcScope 一样长或短,但也由另一个分配的值引用,因此它必须与GcScope 一样长或更长。因此,只有一种解决方案:分配的值必须与它的作用域一样长

    这意味着GcScope的生命周期和它分配的值是完全一样的。 当两个生命周期相同时,Rust 不保证 drop 的顺序。发生这种情况的原因是 drop 实现可能会尝试相互访问,并且由于没有排序,所以它是不安全的(该值可能已经被释放)。

    这在Drop Check chapter of the Rustonomicon中有解释。

    在我们的例子中,垃圾回收状态的 drop 实现并没有取消引用分配的值(恰恰相反,它释放了它们的内存),因此 Rust 编译器过于谨慎,阻止我们实现 drop .

    幸运的是,Nomicon 还解释了如何解决这些具有相同生命周期的值的检查。解决方案是在Drop 实现的生命周期参数上使用may_dangle 属性。 这是不稳定的属性,需要启用generic_param_attrsdropck_eyepatch 功能。

    具体来说,我的drop 实现变成了:

    unsafe impl<'gc> Drop for GcState<'gc> {
        fn drop(&mut self) {
            // Free all the values allocated in this scope
            // Might require changes to make sure there's no use after free
        }
    }
    

    我在lib.rs 中添加了以下几行:

    #![feature(generic_param_attrs)]
    #![feature(dropck_eyepatch)]
    

    您可以阅读有关这些功能的更多信息:

    我更新了我的库 scoped-gc 并修复了此问题,如果您想仔细查看的话。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-01-14
      • 1970-01-01
      • 1970-01-01
      • 2016-07-25
      • 2016-04-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多