Циклы ссылок могут приводить к утечкам памяти
Гарантии безопасности памяти Rust затрудняют, но не делают невозможным
случайное создание памяти, которая никогда не будет очищена (это известно как
утечка памяти). Полное предотвращение утечек памяти не входит в гарантии
Rust, а значит, утечки памяти в Rust безопасны с точки зрения памяти. Мы можем
увидеть, что Rust допускает утечки памяти, используя Rc<T> и RefCell<T>:
можно создать ссылки, в которых элементы ссылаются друг на друга по циклу. Это
создает утечки памяти, потому что счетчик ссылок каждого элемента в цикле
никогда не достигнет 0, и значения никогда не будут удалены.
Создание цикла ссылок
Посмотрим, как может возникнуть цикл ссылок и как его предотвратить, начав с
определения enum 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));
// Uncomment the next line to see that we have a cycle;
// it will overflow the stack.
// println!("a next item = {:?}", a.tail());
}
List, указывающих друг на другаМы создаем экземпляр Rc<List>, содержащий значение List, в переменной a
с исходным списком 5, Nil. Затем мы создаем экземпляр Rc<List>, содержащий
другое значение List, в переменной b; он содержит значение 10 и
указывает на список в a.
Мы изменяем a так, чтобы он указывал на b вместо Nil, создавая цикл. Мы
делаем это с помощью метода 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
Счетчик ссылок экземпляров Rc<List> и в a, и в b равен 2 после того, как
мы изменили список в a, чтобы он указывал на b. В конце 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, 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>.
Сильные ссылки – это способ совместно владеть экземпляром Rc<T>. Слабые
ссылки не выражают отношение владения, и их счетчик не влияет на то, когда
экземпляр Rc<T> очищается. Они не приведут к циклу ссылок, потому что любой
цикл, включающий слабые ссылки, будет разорван, как только счетчик сильных
ссылок участвующих значений станет равен 0.
Когда вы вызываете Rc::downgrade, вы получаете умный указатель типа
Weak<T>. Вместо увеличения strong_count в экземпляре Rc<T> на 1 вызов
Rc::downgrade увеличивает weak_count на 1. Тип Rc<T> использует
weak_count, чтобы отслеживать, сколько ссылок Weak<T> существует, подобно
strong_count. Разница в том, что weak_count не должен быть равен 0, чтобы
экземпляр Rc<T> был очищен.
Поскольку значение, на которое ссылается Weak<T>, могло быть удалено, перед
любой работой со значением, на которое указывает Weak<T>, нужно убедиться,
что значение все еще существует. Для этого вызовите метод upgrade у
экземпляра Weak<T>, который вернет Option<Rc<T>>. Вы получите результат
Some, если значение Rc<T> еще не было удалено, и результат None, если
значение Rc<T> уже удалено. Поскольку 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 владел своими дочерними узлами, и хотим совместно
использовать это владение с переменными, чтобы получать прямой доступ к
каждому Node в дереве. Для этого мы определяем элементы Vec<T> как
значения типа Rc<Node>. Мы также хотим изменять, какие узлы являются
дочерними для другого узла, поэтому в children у нас есть RefCell<T>,
оборачивающий Vec<Rc<Node>>.
Далее мы используем наше определение структуры и создадим один экземпляр
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 является его
родителем. Сделаем это дальше.
Добавление ссылки от дочернего узла к родительскому
Чтобы дочерний узел знал о своем родителе, нужно добавить поле parent в
определение структуры Node. Сложность в том, чтобы решить, каким должен быть
тип 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, у него также будет новая ссылка Weak<Node> в
поле parent, потому что у branch нет родительского узла. При этом leaf
по-прежнему остается одним из дочерних узлов branch. После того как у нас
есть экземпляр Node в branch, мы можем изменить leaf, чтобы дать ему
ссылку Weak<Node> на его родителя. Мы используем метод borrow_mut у
RefCell<Weak<Node>> в поле parent у leaf, а затем используем функцию
Rc::downgrade, чтобы создать ссылку Weak<Node> на branch из Rc<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 хранит клон Rc<Node> из leaf в
branch.children, но счетчик слабых ссылок все еще равен 0.
Когда внутренняя область видимости заканчивается, branch выходит из области
видимости, и счетчик сильных ссылок Rc<Node> уменьшается до 0, поэтому его
Node удаляется. Счетчик слабых ссылок 1 из leaf.parent никак не влияет на
то, будет ли Node удален, поэтому утечки памяти не происходит!
Если мы попробуем получить доступ к родителю 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. Вы даже узнаете о нескольких новых умных указателях.