Rust引用循环和内存泄漏

Rust很难制造内存泄漏(memory leak),但并不是不可能,使用Rc和RefCell创建引用循环会造成内存泄漏,因为每一项的引用计数永远也到不了0,持有的数据也永远不会被释放。

制造引用循环🔗

考虑下面代码:

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
    Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
    fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}
fn main() {
    let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));

    println!("a initial rc count = {}", Rc::strong_count(&a));
    println!("a next item = {:?}", a.tail());

    let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));

    println!("a rc count after b creation = {}", Rc::strong_count(&a));
    println!("b initial rc count = {}", Rc::strong_count(&b));
    println!("b next item = {:?}", b.tail());

    if let Some(link) = a.tail() {
        *link.borrow_mut() = Rc::clone(&b);
    }

    println!("b rc count after changing a = {}", Rc::strong_count(&b));
    println!("a rc count after changing a = {}", Rc::strong_count(&a));

    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack
    // println!("a next item = {:?}", a.tail());
}

这里Cons的第二项是RefCell<Rc<List>>,这里就可以修改Cons指向的List,并且增加了个tail方法来获取Cons的第二项。

在main函数中,我们创建了一个Cons,它的第二项指向了Nil,然后我们创建了一个Cons,它的第二项指向了第一个Cons,后面我们修改了第一个Cons的第二项,使其指向了第二个Cons,这样就形成了一个引用循环。 运行结果:

a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2

可以看到在将第一个Cons的第二项指向第二个Cons后,两个列表的引用计数都是2,在最后,Rust丢弃b,这时第二个实例的引用计数减为1,然后Rust丢弃a,这时第一个实例的引用计数减为1,但是由于这两个实例的引用计数不为0,所以第二个实例不会被释放,这样就形成了一个引用循环。

如果我们取消代码后面注释的println!,Rust就会打印a指向b,然后b指向a,这样就会一直循环下去,直到栈溢出。

如果在更为复杂的程序中的循环里分配了很多内存并占有很长时间,这个程序会使用多余它所需要的内存,有可能压垮系统并造成没有内存可供使用。

对于循环引用,除了使用自动化测试、代码评审等使其最小化,也可以重新组织数据结构,让一部分引用有所有权而另一部分没有,这样只有有所有权的才能影响值是否被丢弃。

将Rc变为Weak🔗

考虑以下代码:

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}

这里定义了一个Node结构体,它有一个值和一个子节点的向量,子节点是Rc<Node>,这样就可以创建一个树,并拥有子节点的所有权,也可以更改子节点。

接下来,我们创建了一个leaf节点和一个branch节点,leaf节点没有子节点,branch节点有一个子节点leaf。

这个时候,我们需要让leaf节点知道branch是他的父节点。

增加子到父的引用🔗

我们需要在Node结构体中增加一个parent字段,这个字段如果是RefCell<Rc<Node>>,就可以存储父节点的引用,但是会形成循环引用。现在换一个思路,如果父节点被丢弃,则子节点也应该被丢弃,如果丢弃子节点,其父节点应该依然存在,这便是弱引用的例子。

我们可以使用Weak<T>来存储父节点的引用,这样就可以避免循环引用:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}

这样子节点就能引用父节点而不拥有父节点,并且使用upgrade方法可以获得包含父节点引用的Option类型。第一个打印时leaf并没有父节点,所以输出为:

leaf parent = None

第二个打印时leaf有父节点,而Weak<Node>会被打印为(Weak),避免了栈溢出的循环,所以输出为:

leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })

可视化strong_count和weak_count的改变🔗

考虑下面代码:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );

    {
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]),
        });

        *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

        println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch),
        );

        println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf),
        );
    }

    println!("leaf parent = {:?}",leaf.parent.borrow().upgrade());
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );
}

一开始创建了leaf,它的强引用计数为1,弱引用计数为0,后面内部作用域创建branch后,branch的强引用计数为1,弱引用计数为1,leaf的强引用计数为2,弱引用计数为0,后面离开内部作用域,branch的强引用计数为0,被丢弃,leaf的强引用计数为1,弱引用计数为0。