7 releases
0.2.6 | Jan 31, 2023 |
---|---|
0.2.5 |
|
0.1.2 | Jan 29, 2023 |
#536 in Memory management
35KB
696 lines
基于作用域的安全GC设计
使用
首先,假设你需要管理的类型为
struct A<'n>(&'n i32);
要使得它可以被 GC 管理,需要为其实现 scope_gc::target::Target
impl<'n> Target for A<'n> {
type RefObject<'gc> = StrongRef<'gc, dyn NodeA<'gc>>;
unsafe fn pre_drop<'gc>(&self, _ref_set: &Self::RefObject<'gc>) {
println!("pre-drop A");
}
}
RefObject
指的是与该类型匹配的引用集合,一旦值进入 GC 系统,我们就会在内部为其创建一个 RefObject
只有 RefObject
可以拥有指向被管理对象的引用,并且会追踪自身拥有的全部引用
pre_drop
将在 GC 确定要销毁,但还没有执行销毁时调用,此时,RefObject
持有的引用依然有效
pre_drop
过程不可逆,因此,任何试图修改引用结构以使得引用增加的行为都是未定义的
在销毁之前 pre_drop
会保证被调用
当然,为值实现 Drop
也是可行的,并且同样保证调用,但此时已经无法访问 GC 引用了
impl<'n> Drop for A<'n> {
fn drop(&mut self) {
println!("drop A");
}
}
将值加入 GC 后,可以得到一个对应类型的 RootRef<'gc, Node<'gc, T>>
Node<'gc, T>
是 RefObject<'gc>
和 T
类型的值的总和
因此,你可以通过定义自己的 trait 来为 Node<'gc, T>
添加行为
trait NodeA<'gc>: NodeTrait<'gc> {
fn inner(&self) -> &i32;
}
impl<'gc, 'n> NodeA<'gc> for Node<'gc, A<'n>> {
fn inner(&self) -> &i32 {
&self.deref().0
}
}
这样,你就可以在 RootRef<'gc, dyn NodeA<'gc>>
中调用 inner
了
整体的用法类似于
fn test() {
let config = Config {
pre_drop: true,
..Default::default()
};
let i = 1;
scope_gc(config, |gc: Gc| {
let x = gc.new(A(&i));
let y = gc.new(A(&i));
x.ref_set().set_ref(y.deref());
y.ref_set().set_ref(x.deref());
let _p = x.inner();
println!("{:#?}", gc);
gc.clear();
println!("{:#?}", gc);
});
}
GC::new
只能接受存活时间长于闭包的值,具体原因如后文所述
使用 GC::forget
接受存活时间较短的值,但执行回收时仅仅回收内存,其预析构和析构函数均不会被调用
使用 unsafe GC::dangling
接受存活时间较短的值,且执行与 GC::new
同样的逻辑
使用 GC::new_raw(x)
其等价于 GC::new(RawType(x))
,不过不推荐如此,在此情况下,使用来自 Rc
无疑是更好的选择
在 Rust 中引入 GC 所存在的问题
销毁问题
用户可以自行实现 std::ops::Drop
来为自定义类型制定销毁动作
struct A<'s>(&'s i32);
impl Drop for A {
fn drop(&mut self) {
println!("drop A({})", self.0);
}
}
如果简单的在栈上创建值,编译器会确保 drop
在合适的时刻被调用,然而,复杂的内存管理策略却无法保证调用是及时的
例如,如果使用 std::rc::Rc
来管理 A<'s>
,并且创建了一个循环引用,值将会存活的比 's
还久
倘若我们错过了 drop
的合理调用时刻,在此之后 's
进入悬空状态,被其修饰的内容都是悬空的,调用 drop
就可能导致未定义的行为