Циклы ссылок и утечки памяти
Гарантии безопасности памяти в Rust затрудняют, но не исключают полностью
утечку памяти (memory leak) — ситуацию, когда память выделена и случайно
так никогда и не освобождается. Полностью предотвращать утечки Rust не
гарантирует — то есть утечка памяти в Rust считается безопасной для памяти.
Что Rust допускает утечки, видно на примере Rc<T> и RefCell<T>: с ними
можно создать ссылки, при которых элементы ссылаются друг на друга по кругу.
Это и приводит к утечке: счётчик ссылок каждого элемента в таком цикле никогда
не опустится до 0, и значения никогда не будут уничтожены.
Создание цикла ссылок
Посмотрим, как может возникнуть цикл ссылок и как его предотвратить. Начнём с
определения перечисления List и метода tail из листинга 15-25.
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() {}
RefCell<T>, позволяющим менять то, на что ссылается вариант ConsЭто ещё один вариант определения List из листинга 15-5. Второй элемент
варианта Cons теперь имеет тип RefCell<Rc<List>>. То есть менять мы хотим
не значение i32, как в листинге 15-24, а то значение List, на которое
указывает вариант Cons. Ещё мы добавили метод tail, чтобы удобно получать
второй элемент варианта Cons.
В листинге 15-26 мы добавляем функцию main, которая использует определения из
листинга 15-25. Этот код создаёт список в a и список в b, указывающий на
список в a. Затем он меняет список в a так, чтобы тот указывал на b, — и
получается цикл ссылок. По ходу дела вызовы println! показывают, какими
бывают счётчики ссылок в разные моменты.
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));
// Раскомментируйте следующую строку, чтобы убедиться, что получился цикл;
// она переполнит стек.
// println!("a next item = {:?}", a.tail());
}
List, указывающих друг на другаМы создаём в переменной a экземпляр Rc<List> со значением List —
начальным списком 5, Nil. Затем создаём в переменной b экземпляр Rc<List>
с другим значением List, которое содержит 10 и указывает на список в a.
Затем меняем a так, чтобы он указывал не на Nil, а на b, — так и
возникает цикл. Для этого методом tail получаем ссылку на
RefCell<Rc<List>> в a и кладём её в переменную link. Затем методом
borrow_mut у RefCell<Rc<List>> меняем внутреннее значение: вместо
Rc<List> со значением Nil теперь там Rc<List> из b.
Если запустить этот код, пока оставив последний println! закомментированным,
получим:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/cons-list`
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
После того как список в a начал указывать на b, счётчики ссылок у
экземпляров Rc<List> и в a, и в b равны 2. В конце main Rust уничтожает
переменную b, и счётчик ссылок экземпляра Rc<List> из b уменьшается с 2
до 1. Память, которую этот Rc<List> занимает в куче, в этот момент не
освобождается: счётчик равен 1, а не 0. Затем Rust уничтожает a, и счётчик
экземпляра Rc<List> из a тоже уменьшается с 2 до 1. Его память тоже не
освободить: на него всё ещё ссылается другой экземпляр Rc<List>. Память,
выделенная под списки, так и останется неосвобождённой навсегда. Чтобы
наглядно показать этот цикл ссылок, мы нарисовали схему на рисунке 15-4.
Рисунок 15-4: Цикл ссылок из списков a и b,
указывающих друг на друга
Если раскомментировать последний println! и запустить программу, Rust
попытается вывести этот цикл: a указывает на b, который указывает на a, и
так далее, пока не переполнится стек.
По сравнению с настоящей программой последствия цикла ссылок в этом примере не так страшны: сразу после создания цикла программа завершается. Но если бы сложная программа выделила в цикле много памяти и долго её удерживала, она расходовала бы больше памяти, чем нужно, и могла бы перегрузить систему — вплоть до того, что память закончится.
Создать цикл ссылок непросто, но возможно. Если у вас есть значения
RefCell<T>, содержащие значения Rc<T>, или похожие вложенные сочетания типов
с внутренней изменяемостью и подсчётом ссылок, следите за тем, чтобы не
создавать циклов: рассчитывать, что их поймает Rust, нельзя. Цикл ссылок — это
логическая ошибка в программе, и бороться с ней нужно автоматическими
тестами, ревью кода и другими практиками разработки.
Другой способ избежать циклов ссылок — перестроить структуры данных так, чтобы
одни ссылки выражали владение, а другие — нет. Тогда циклы могут состоять из
отношений владения и отношений без владения, и на то, можно ли уничтожить
значение, влияют только отношения владения. В листинге 15-25 варианты Cons
всегда должны владеть своим списком, так что перестроить структуру данных не
получится. Рассмотрим пример с графами из родительских и дочерних узлов и
увидим, когда отношения без владения — подходящий способ избежать циклов
ссылок.
Предотвращаем циклы ссылок с помощью Weak<T>
Мы уже видели, что вызов Rc::clone увеличивает strong_count экземпляра
Rc<T>, а экземпляр Rc<T> освобождается, только когда его strong_count
равен 0. Но можно создать и слабую ссылку на значение внутри экземпляра
Rc<T>: для этого вызывают Rc::downgrade и передают ссылку на Rc<T>.
Сильные ссылки (strong references) — это способ совместно владеть
экземпляром Rc<T>. Слабые ссылки (weak references) отношения владения не
выражают, и их количество не влияет на то, когда экземпляр Rc<T> будет
освобождён. Цикла ссылок они не создадут: любой цикл, в котором есть слабые
ссылки, разорвётся, как только счётчик сильных ссылок участвующих значений
станет равен 0.
Вызов Rc::downgrade возвращает умный указатель типа Weak<T>. Вместо того
чтобы увеличивать на 1 strong_count экземпляра Rc<T>, Rc::downgrade
увеличивает на 1 weak_count. Через weak_count тип Rc<T> отслеживает,
сколько существует ссылок Weak<T>, — так же, как через strong_count.
Разница в том, что для освобождения экземпляра Rc<T> weak_count не обязан
быть равен 0.
Значение, на которое ссылается Weak<T>, могло уже быть уничтожено. Поэтому,
чтобы что-то сделать со значением, на которое указывает Weak<T>, нужно
убедиться, что оно ещё существует. Для этого у экземпляра Weak<T> вызывают
метод upgrade, который возвращает Option<Rc<T>>. Если значение Rc<T> ещё
не уничтожено, результат будет Some, а если уже уничтожено — None. Раз
upgrade возвращает Option<Rc<T>>, Rust проследит, чтобы были обработаны оба
случая — и Some, и None, — и недействительного указателя не будет.
Для примера вместо списка, элементы которого знают только о следующем элементе, создадим дерево, элементы которого знают и о своих детях, и о своём родителе.
Создаём дерево
Для начала построим дерево, узлы которого знают о своих дочерних узлах.
Создадим структуру Node, которая хранит собственное значение i32 и ссылки
на дочерние значения Node:
Файл: src/main.rs
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 владел своими детьми, и при этом хотим делить это
владение с переменными, чтобы обращаться к любому узлу дерева напрямую. Для
этого элементы Vec<T> у нас будут типа Rc<Node>. Ещё мы хотим менять, какие
узлы являются детьми другого узла, поэтому в поле children вектор
Vec<Rc<Node>> обёрнут в RefCell<T>.
Теперь с помощью этого определения создадим экземпляр Node с именем leaf
(лист), значением 3 и без детей, а также экземпляр branch (ветка) со
значением 5, у которого leaf — один из детей, как в листинге 15-27.
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)]),
});
}
leaf без детей и узел branch, у которого leaf — один из детейМы клонируем Rc<Node> из leaf и сохраняем его в branch. Значит, у Node в
leaf теперь два владельца: leaf и branch. От branch к leaf можно
добраться через branch.children, а вот от leaf к branch — никак: у leaf
нет ссылки на branch, и он не знает, что они связаны. Мы хотим, чтобы leaf
знал, что branch — его родитель. Этим и займёмся.
Ссылка от ребёнка к родителю
Чтобы дочерний узел знал о родителе, добавим в определение структуры Node
поле parent. Трудность в том, какой тип выбрать для parent. Rc<T> здесь
не подходит: получился бы цикл ссылок — leaf.parent указывал бы на branch,
а branch.children — на leaf, и их strong_count никогда не опустились бы до
0.
Посмотрим на отношения с другой стороны: родительский узел должен владеть детьми — если родитель уничтожается, его дочерние узлы тоже должны уничтожаться. А вот ребёнок не должен владеть родителем: если уничтожить дочерний узел, родитель должен остаться. Это как раз случай для слабых ссылок!
Так что вместо Rc<T> сделаем тип parent на основе Weak<T>, а точнее —
RefCell<Weak<Node>>. Теперь определение структуры Node выглядит так:
Файл: src/main.rs
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());
}
Узел сможет ссылаться на родителя, но не будет им владеть. В листинге 15-28 мы
обновляем main под новое определение, чтобы у узла leaf был способ
сослаться на своего родителя, branch.
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());
}
leaf со слабой ссылкой на родительский узел branchСоздание узла leaf похоже на листинг 15-27, кроме поля parent: сначала у
leaf нет родителя, поэтому мы создаём новый пустой экземпляр ссылки
Weak<Node>.
Если сейчас попытаться получить ссылку на родителя leaf методом upgrade,
получим None. Это видно по выводу первого println!:
leaf parent = None
Узел branch при создании тоже получит в поле parent новую ссылку
Weak<Node>, ведь у branch родителя нет. leaf по-прежнему один из детей
branch. Когда экземпляр Node в branch создан, можно изменить leaf и
дать ему ссылку Weak<Node> на родителя. Вызываем borrow_mut у RefCell<Weak<Node>>
в поле parent узла leaf, а затем функцией Rc::downgrade создаём из
Rc<Node> в branch ссылку Weak<Node> на branch.
Если снова вывести родителя leaf, на этот раз получим вариант Some с
branch внутри: теперь leaf может обратиться к родителю! А при выводе leaf
не возникает цикла, который в листинге 15-26 заканчивался переполнением стека:
ссылки 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: [] } }] } })
Отсутствие бесконечного вывода говорит о том, что этот код не создал цикл
ссылок. Это видно и по значениям, которые возвращают Rc::strong_count и
Rc::weak_count.
Как меняются strong_count и weak_count
Посмотрим, как меняются значения strong_count и weak_count у экземпляров
Rc<Node>. Для этого создадим новую внутреннюю область видимости и перенесём
в неё создание branch. Так видно, что происходит, когда branch создаётся, а
затем уничтожается при выходе из области видимости. Изменения — в листинге
15-29.
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),
);
}
branch во внутренней области видимости и смотрим на счётчики сильных и слабых ссылокПосле создания leaf у его Rc<Node> счётчик сильных ссылок равен 1, а
слабых — 0. Во внутренней области мы создаём branch и связываем его с
leaf. Если в этот момент вывести счётчики, у Rc<Node> в branch будет 1
сильная ссылка и 1 слабая (это leaf.parent, указывающий на branch через
Weak<Node>). А у leaf будет 2 сильные ссылки — ведь branch теперь хранит в
branch.children клон Rc<Node> узла leaf, — и по-прежнему 0 слабых.
Когда внутренняя область заканчивается, branch выходит из области видимости,
счётчик сильных ссылок его Rc<Node> падает до 0, и его Node уничтожается.
Одна слабая ссылка из leaf.parent на это никак не влияет, так что утечки
памяти нет!
Если после конца области видимости попытаться обратиться к родителю leaf,
снова получим None. В конце программы у Rc<Node> в leaf 1 сильная ссылка и
0 слабых: переменная leaf снова единственная ссылка на этот Rc<Node>.
Вся логика подсчёта и уничтожения значений встроена в Rc<T> и Weak<T> и в
их реализации трейта Drop. Указав в определении Node, что связь от ребёнка
к родителю — ссылка Weak<T>, мы можем сделать так, чтобы родители указывали
на детей, а дети на родителей, и при этом не возникало ни циклов ссылок, ни
утечек памяти.
Итоги
В этой главе мы разобрали, как с помощью умных указателей получать другие
гарантии и компромиссы, чем те, что Rust даёт по умолчанию с обычными
ссылками. Тип Box<T> имеет известный размер и указывает на данные в куче. Тип
Rc<T> считает ссылки на данные в куче, чтобы у данных могло быть несколько
владельцев. Тип RefCell<T> с его внутренней изменяемостью пригодится, когда
нужен неизменяемый тип, но внутреннее значение этого типа надо менять; к тому
же он проверяет правила заимствования не при компиляции, а во время
выполнения.
Ещё мы обсудили трейты Deref и Drop, на которых держится большая часть
возможностей умных указателей. И разобрали циклы ссылок, которые могут
приводить к утечкам памяти, а также то, как предотвращать их с помощью
Weak<T>.
Если эта глава вас заинтересовала и вы хотите писать собственные умные указатели, загляните в «The Rustonomicon» — там много полезного.
Дальше поговорим о конкурентности в Rust. Заодно вы познакомитесь с ещё несколькими умными указателями.