Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Циклы ссылок и утечки памяти

Гарантии безопасности памяти в Rust затрудняют, но не исключают полностью утечку памяти (memory leak) — ситуацию, когда память выделена и случайно так никогда и не освобождается. Полностью предотвращать утечки Rust не гарантирует — то есть утечка памяти в Rust считается безопасной для памяти. Что Rust допускает утечки, видно на примере Rc<T> и RefCell<T>: с ними можно создать ссылки, при которых элементы ссылаются друг на друга по кругу. Это и приводит к утечке: счётчик ссылок каждого элемента в таком цикле никогда не опустится до 0, и значения никогда не будут уничтожены.

Создание цикла ссылок

Посмотрим, как может возникнуть цикл ссылок и как его предотвратить. Начнём с определения перечисления List и метода tail из листинга 15-25.

Файл: src/main.rs
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() {}
Листинг 15-25: Определение cons-списка с 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! показывают, какими бывают счётчики ссылок в разные моменты.

Файл: src/main.rs
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());
}
Листинг 15-26: Создаём цикл ссылок из двух значений 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.

Прямоугольник с меткой 'a' указывает на прямоугольник с числом 5. Прямоугольник с меткой 'b' указывает на прямоугольник с числом 10. Прямоугольник с 5 указывает на прямоугольник с 10, а прямоугольник с 10 — обратно на прямоугольник с 5: получается цикл.

Рисунок 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.

Файл: 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)]),
    });
}
Листинг 15-27: Создаём узел 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.

Файл: 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: Узел 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.

Файл: 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 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),
    );
}
Листинг 15-29: Создаём 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. Заодно вы познакомитесь с ещё несколькими умными указателями.