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

Les cycles de références peuvent provoquer des fuites de mémoire

Les garanties de sécurité de la mémoire de Rust rendent difficile, mais pas impossible, la création accidentelle de mémoire qui n’est jamais nettoyée (connue sous le nom de fuite de mémoire). Prévenir entièrement les fuites de mémoire ne fait pas partie des garanties de Rust, ce qui signifie que les fuites de mémoire sont considérées comme sûres en mémoire en Rust. Nous pouvons constater que Rust permet les fuites de mémoire en utilisant Rc<T> et RefCell<T> : il est possible de créer des références où les éléments se réfèrent les uns aux autres dans un cycle. Cela crée des fuites de mémoire car le compteur de références de chaque élément dans le cycle n’atteindra jamais 0, et les valeurs ne seront jamais libérées.

Créer un cycle de références

Voyons comment un cycle de références peut se produire et comment le prévenir, en commençant par la définition de l’enum List et une méthode tail dans l’encart 15-25.

Filename: 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() {}
Listing 15-25: A cons list definition that holds a RefCell<T> so that we can modify what a Cons variant is referring to

Nous utilisons une autre variation de la définition de List de l’encart 15-5. Le deuxième élément de la variante Cons est maintenant RefCell<Rc<List>>, ce qui signifie qu’au lieu de pouvoir modifier la valeur i32 comme nous l’avons fait dans l’encart 15-24, nous voulons modifier la valeur List vers laquelle une variante Cons pointe. Nous ajoutons aussi une méthode tail pour faciliter l’accès au deuxième élément si nous avons une variante Cons.

Dans l’encart 15-26, nous ajoutons une fonction main qui utilise les définitions de l’encart 15-25. Ce code crée une liste dans a et une liste dans b qui pointe vers la liste dans a. Ensuite, il modifié la liste dans a pour pointer vers b, créant un cycle de références. Il y à des instructions println! tout au long du processus pour montrer quels sont les compteurs de références à différents moments.

Filename: 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));

    // Uncomment the next line to see that we have a cycle;
    // it will overflow the stack.
    // println!("a next item = {:?}", a.tail());
}
Listing 15-26: Creating a reference cycle of two List values pointing to each other

Nous créons une instance Rc<List> contenant une valeur List dans la variable a avec une liste initiale de 5, Nil. Nous créons ensuite une instance Rc<List> contenant une autre valeur List dans la variable b qui contient la valeur 10 et pointe vers la liste dans a.

Nous modifions a pour qu’il pointe vers b au lieu de Nil, créant un cycle. Nous le faisons en utilisant la méthode tail pour obtenir une référence vers le RefCell<Rc<List>> dans a, que nous mettons dans la variable link. Ensuite, nous utilisons la méthode borrow_mut sur le RefCell<Rc<List>> pour changer la valeur à l’intérieur d’un Rc<List> contenant une valeur Nil vers le Rc<List> dans b.

Quand nous exécutons ce code, en gardant le dernier println! commenté pour le moment, nous obtenons cette sortie : console {{#include ../listings/ch15-smart-pointers/listing-15-26/output.txt}}

$ 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

Le compteur de références des instances Rc<List> dans a et b est de 2 après que nous avons modifié la liste dans a pour pointer vers b. À la fin de main, Rust libère la variable b, ce qui diminue le compteur de références de l’instance Rc<List> de b de 2 à 1. La mémoire que Rc<List> occupe sur le tas ne sera pas libérée à ce stade car son compteur de références est 1, pas 0. Ensuite, Rust libère a, ce qui diminue aussi le compteur de références de l’instance Rc<List> de a de 2 à 1. La mémoire de cette instance ne peut pas non plus être libérée, car l’autre instance Rc<List> y fait encore référence. La mémoire allouée à la liste restera non récupérée pour toujours. Pour visualiser ce cycle de références, nous avons créé le diagramme de la figure 15-4.

Un rectangle étiqueté « a » qui pointe vers un rectangle contenant l’entier 5. Un rectangle étiqueté « b » qui pointe vers un rectangle contenant l’entier 10. Le rectangle contenant 5 pointe vers le rectangle contenant 10, et le rectangle contenant 10 pointe à son tour vers le rectangle contenant 5, créant ainsi un cycle.

Figure 15-4 : Un cycle de références des listes a et b pointant l’une vers l’autre

Si vous décommentez le dernier println! et exécutez le programme, Rust essaiera d’afficher ce cycle avec a pointant vers b pointant vers a et ainsi de suite jusqu’à ce qu’il déborde la pile.

Comparé à un programme réel, les conséquences de la création d’un cycle de références dans cet exemple ne sont pas très graves : juste après avoir créé le cycle de références, le programme se terminé. Cependant, si un programme plus complexe allouait beaucoup de mémoire dans un cycle et la conservait longtemps, le programme utiliserait plus de mémoire que nécessaire et pourrait submerger le système, le faisant manquer de mémoire disponible.

Créer des cycles de références ne se fait pas facilement, mais ce n’est pas impossible non plus. Si vous avez des valeurs RefCell<T> qui contiennent des valeurs Rc<T> ou des combinaisons imbriquées similaires de types avec mutabilité intérieure et comptage de références, vous devez vous assurer de ne pas créer de cycles ; vous ne pouvez pas compter sur Rust pour les détecter. Créer un cycle de références serait un bogue logique dans votre programme que vous devriez minimiser en utilisant des tests automatisés, des revues de code et d’autres pratiques de développement logiciel.

Une autre solution pour éviter les cycles de références est de réorganiser vos structures de données de sorte que certaines références expriment la possession et d’autres non. Ainsi, vous pouvez avoir des cycles composés de certaines relations de possession et de certaines relations sans possession, et seules les relations de possession affectent si une valeur peut être libérée ou non. Dans l’encart 15-25, nous voulons toujours que les variantes Cons possèdent leur liste, donc réorganiser la structure de données n’est pas possible. Examinons un exemple utilisant des graphes composés de noeuds parents et de noeuds enfants pour voir quand les relations sans possession sont un moyen approprié de prévenir les cycles de références.

Prévenir les cycles de références avec Weak<T>

Jusqu’ici, nous avons démontré que l’appel à Rc::clone augmente le strong_count d’une instance Rc<T>, et qu’une instance Rc<T> n’est nettoyée que si son strong_count est à 0. Vous pouvez aussi créer une référence faible vers la valeur dans une instance Rc<T> en appelant Rc::downgrade et en passant une référence vers le Rc<T>. Les références fortes sont la façon dont vous pouvez partager la possession d’une instance Rc<T>. Les références faibles n’expriment pas une relation de possession, et leur compteur n’affecte pas le moment où une instance Rc<T> est nettoyée. Elles ne causeront pas de cycle de références, car tout cycle impliquant des références faibles sera brisé une fois que le compteur de références fortes des valeurs impliquées est à 0.

Quand vous appelez Rc::downgrade, vous obtenez un pointeur intelligent de type Weak<T>. Au lieu d’augmenter le strong_count dans l’instance Rc<T> de 1, l’appel à Rc::downgrade augmente le weak_count de 1. Le type Rc<T> utilise weak_count pour suivre combien de références Weak<T> existent, de manière similaire à strong_count. La différence est que le weak_count n’a pas besoin d’être à 0 pour que l’instance Rc<T> soit nettoyée.

Comme la valeur que Weak<T> référence pourrait avoir été libérée, pour faire quoi que ce soit avec la valeur vers laquelle un Weak<T> pointe, vous devez vous assurer que la valeur existe encore. Faites-le en appelant la méthode upgrade sur une instance Weak<T>, qui retournera un Option<Rc<T>>. Vous obtiendrez un résultat Some si la valeur Rc<T> n’a pas encore été libérée et un résultat None si la valeur Rc<T> a été libérée. Comme upgrade retourné un Option<Rc<T>>, Rust s’assurera que les cas Some et None sont gérés, et il n’y aura pas de pointeur invalide.

Comme exemple, plutôt que d’utiliser une liste dont les éléments ne connaissent que l’élément suivant, nous allons créer un arbre dont les éléments connaissent leurs éléments enfants et leurs éléments parents.

Créer une structure de données en arbre

Pour commencer, nous allons construire un arbre avec des noeuds qui connaissent leurs noeuds enfants. Nous allons créer une struct nommée Node qui contient sa propre valeur i32 ainsi que des références vers les valeurs de ses Node enfants : Filename: src/main.rs rust {{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-27/src/main.rs:here}}

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

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)]),
    });
}

Nous voulons qu’un Node possède ses enfants, et nous voulons partager cette possession avec des variables afin de pouvoir accéder directement à chaque Node dans l’arbre. Pour ce faire, nous définissons les éléments du Vec<T> comme des valeurs de type Rc<Node>. Nous voulons aussi modifier quels noeuds sont les enfants d’un autre noeud, donc nous avons un RefCell<T> dans children autour du Vec<Rc<Node>>.

Ensuite, nous utiliserons notre définition de struct pour créer une instance Node nommée leaf avec la valeur 3 et sans enfants, et une autre instance nommée branch avec la valeur 5 et leaf comme l’un de ses enfants, comme montré dans l’encart 15-27.

Filename: 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)]),
    });
}
Listing 15-27: Creating a leaf node with no children and a branch node with leaf as one of its children

Nous clonons le Rc<Node> dans leaf et le stockons dans branch, ce qui signifie que le Node dans leaf a maintenant deux propriétaires : leaf et branch. Nous pouvons aller de branch à leaf via branch.children, mais il n’y à aucun moyen d’aller de leaf à branch. La raison est que leaf n’a pas de référence vers branch et ne sait pas qu’ils sont liés. Nous voulons que leaf sache que branch est son parent. Nous ferons cela ensuite.

Ajouter une référence d’un enfant vers son parent

Pour que le noeud enfant connaisse son parent, nous devons ajouter un champ parent à notre définition de la struct Node. La difficulté réside dans le choix du type de parent. Nous savons qu’il ne peut pas contenir un Rc<T>, car cela créerait un cycle de références avec leaf.parent pointant vers branch et branch.children pointant vers leaf, ce qui ferait que leurs valeurs strong_count ne seraient jamais à 0.

En réfléchissant aux relations d’une autre manière, un noeud parent devrait posséder ses enfants : si un noeud parent est libéré, ses noeuds enfants devraient l’être aussi. Cependant, un enfant ne devrait pas posséder son parent : si nous libérons un noeud enfant, le parent devrait toujours exister. C’est un cas d’utilisation pour les références faibles !

Donc, au lieu de Rc<T>, nous allons faire en sorte que le type de parent utilise Weak<T>, spécifiquement un RefCell<Weak<Node>>. Maintenant, notre définition de la struct Node ressemble à ceci : Filename: src/main.rs rust {{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-28/src/main.rs:here}}

Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}

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());
}

Un noeud pourra faire référence à son noeud parent mais ne possède pas son parent. Dans l’encart 15-28, nous mettons à jour main pour utiliser cette nouvelle définition afin que le noeud leaf ait un moyen de faire référence à son parent, branch.

Filename: 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());
}
Listing 15-28: A leaf node with a weak reference to its parent node, branch

La création du noeud leaf ressemble à l’encart 15-27 à l’exception du champ parent : leaf commence sans parent, donc nous créons une nouvelle instance de référence Weak<Node> vide.

À ce stade, quand nous essayons d’obtenir une référence vers le parent de leaf en utilisant la méthode upgrade, nous obtenons une valeur None. Nous voyons cela dans la sortie de la première instruction println! : text leaf parent = None

leaf parent = None

Quand nous créons le noeud branch, il aura aussi une nouvelle référence Weak<Node> dans le champ parent car branch n’a pas de noeud parent. Nous avons toujours leaf comme l’un des enfants de branch. Une fois que nous avons l’instance Node dans branch, nous pouvons modifier leaf pour lui donner une référence Weak<Node> vers son parent. Nous utilisons la méthode borrow_mut sur le RefCell<Weak<Node>> dans le champ parent de leaf, puis nous utilisons la fonction Rc::downgrade pour créer une référence Weak<Node> vers branch à partir du Rc<Node> dans branch.

Quand nous affichons le parent de leaf à nouveau, cette fois nous obtenons une variante Some contenant branch : maintenant leaf peut accéder à son parent ! Quand nous affichons leaf, nous évitons aussi le cycle qui finissait par un débordement de pile comme dans l’encart 15-26 ; les références Weak<Node> sont affichées comme (Weak) : text leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } })

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

L’absence de sortie infinie indique que ce code n’a pas créé de cycle de références. Nous pouvons aussi le constater en regardant les valeurs que nous obtenons en appelant Rc::strong_count et Rc::weak_count.

Visualiser les changements de strong_count et weak_count

Voyons comment les valeurs strong_count et weak_count des instances Rc<Node> changent en créant une nouvelle portée intérieure et en déplaçant la création de branch dans cette portée. Ce faisant, nous pouvons voir ce qui se passe quand branch est créé puis libéré quand il sort de la portée. Les modifications sont montrées dans l’encart 15-29.

Filename: 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),
    );
}
Listing 15-29: Creating branch in an inner scope and examining strong and weak reference counts

Après la création de leaf, son Rc<Node> à un compteur fort de 1 et un compteur faible de 0. Dans la portée intérieure, nous créons branch et l’associons à leaf, à ce moment-là, quand nous affichons les compteurs, le Rc<Node> dans branch aura un compteur fort de 1 et un compteur faible de 1 (pour leaf.parent pointant vers branch avec un Weak<Node>). Quand nous affichons les compteurs dans leaf, nous verrons qu’il aura un compteur fort de 2 car branch a maintenant un clone du Rc<Node> de leaf stocké dans branch.children mais aura toujours un compteur faible de 0.

Quand la portée intérieure se terminé, branch sort de la portée et le compteur fort du Rc<Node> diminue à 0, donc son Node est libéré. Le compteur faible de 1 provenant de leaf.parent n’à aucune incidence sur le fait que Node soit libéré ou non, donc nous n’avons aucune fuite de mémoire !

Si nous essayons d’accéder au parent de leaf après la fin de la portée, nous obtiendrons à nouveau None. À la fin du programme, le Rc<Node> dans leaf à un compteur fort de 1 et un compteur faible de 0 car la variable leaf est à nouveau la seule référence vers le Rc<Node>.

Toute la logique qui gère les compteurs et la libération des valeurs est intégrée dans Rc<T> et Weak<T> et dans leurs implémentations du trait Drop. En spécifiant que la relation d’un enfant vers son parent devrait être une référence Weak<T> dans la définition de Node, vous pouvez avoir des noeuds parents pointant vers des noeuds enfants et vice versa sans créer de cycle de références ni de fuites de mémoire.

Résumé

Ce chapitre a couvert comment utiliser les pointeurs intelligents pour obtenir des garanties et des compromis différents de ceux que Rust offre par défaut avec les références classiques. Le type Box<T> à une taille connue et pointe vers des données allouées sur le tas. Le type Rc<T> suit le nombre de références vers des données sur le tas afin que les données puissent avoir plusieurs propriétaires. Le type RefCell<T> avec sa mutabilité intérieure nous donne un type que nous pouvons utiliser quand nous avons besoin d’un type immuable mais devons changer une valeur intérieure de ce type ; il applique aussi les règles d’emprunt à l’exécution plutôt qu’à la compilation.

Nous avons aussi discuté des traits Deref et Drop, qui permettent une grande partie de la fonctionnalité des pointeurs intelligents. Nous avons exploré les cycles de références qui peuvent provoquer des fuites de mémoire et comment les prévenir en utilisant Weak<T>.

Si ce chapitre a piqué votre curiosité et que vous voulez implémenter vos propres pointeurs intelligents, consultez [“The Rustonomicon”][nomicon] pour plus d’informations utiles.

Ensuite, nous parlerons de la concurrence en Rust. Vous apprendrez même quelques nouveaux pointeurs intelligents.