Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories

SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI

Le Perdite di Memoria causate dai Cicli di Riferimento

Le garanzie di sicurezza della memoria di Rust rendono difficile, ma non impossibile, creare accidentalmente memoria che non viene mai liberata (nota come perdita di memoria). Prevenire completamente le perdite di memoria non è una delle garanzie di Rust, il che significa che le perdite di memoria sono sicure nella memoria di Rust. Possiamo vedere che Rust permette le perdite di memoria utilizzando Rc<T> e RefCell<T>: è possibile creare riferimenti in cui gli elementi si riferiscono l’un l’altro in un ciclo. Ciò crea perdite di memoria perché il conteggio dei riferimenti di ciascun elemento nel ciclo non raggiungerà mai 0 e i valori non verranno mai eliminati.

Creare un Ciclo di Riferimento

Vediamo come potrebbe verificarsi un ciclo di riferimento e come prevenirlo, a partire dalla definizione dell’enum List e di un metodo tail nel Listing 15-25:

rust

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: Definizione di una lista cons che tiene una RefCell<T> in modo da poter modificare a cosa si riferisce una variante Cons

Stiamo utilizzando un’altra variante della definizione di List rispetto al Listing 15-5. Il secondo elemento nella variante Cons è ora RefCell<Rc<List>>, il che significa che invece di avere la possibilità di modificare il valore i32 come abbiamo fatto nel Listing 15-24, vogliamo modificare il valore List a cui si riferisce una variante Cons. Stiamo anche aggiungendo un metodo tail per rendere conveniente l’accesso al secondo elemento se abbiamo una variante Cons.

Nel Listing 15-26, stiamo aggiungendo una funzione main che utilizza le definizioni nel Listing 15-25. Questo codice crea una lista in a e una lista in b che punta alla lista in a. Quindi modifica la lista in a per puntare a b, creando un ciclo di riferimento. Ci sono istruzioni println! lungo il percorso per mostrare quali sono i conteggi dei riferimenti in vari punti di questo processo.

rust

Filename: src/main.rs 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));
}

Listing 15-26: Creare un ciclo di riferimento di due valori di Lista che si puntano a vicenda

Creiamo un’istanza Rc<List> che tiene un valore List nella variabile a con un elenco iniziale di 5, Nil. Quindi creiamo un’istanza Rc<List> che tiene un altro valore List nella variabile b che contiene il valore 10 e punta all’elenco in a.

Modifichiamo a in modo che punti a b invece che a Nil, creando un ciclo. Facciamo ciò utilizzando il metodo tail per ottenere un riferimento a RefCell<Rc<List>> in a, che mettiamo nella variabile link. Quindi utilizziamo il metodo borrow_mut su RefCell<Rc<List>> per cambiare il valore interno da un Rc<List> che contiene un valore Nil a Rc<List> in b.

Quando eseguiamo questo codice, mantenendo il println! finale commentato per il momento, otterremo questa output:

rust

$ cargo run
Compilazione cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [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

Il conteggio dei riferimenti delle istanze Rc<List> sia in a che in b è 2 dopo aver modificato l’elenco in a per puntare a b. Alla fine di main, Rust elimina la variabile b, che diminuisce il conteggio dei riferimenti dell’istanza Rc<List> di b da 2 a 1. La memoria che Rc<List> ha nell’heap non verrà eliminata in questo momento, perché il suo conteggio dei riferimenti è 1, non 0. Poi Rust elimina a, che diminuisce il conteggio dei riferimenti dell’istanza Rc<List> di a da 2 a 1 anche. La memoria allocata per l’elenco rimarrà non raccolta per sempre. Per visualizzare questo ciclo di riferimento, abbiamo creato un diagramma nella Figura 15-4.

Ciclo di Riferimento delle Liste

Figura 15-4: Un ciclo di riferimento delle liste a e b che si puntano a vicenda

Se rimuovi il commento dall’ultimo println! e esegui il programma, Rust cercherà di stampare questo ciclo con a che punta a b che punta a a e così via finché non supererà lo stack.

Rispetto a un programma del mondo reale, le conseguenze di creare un ciclo di riferimento in questo esempio non sono molto gravi: subito dopo aver creato il ciclo di riferimento, il programma termina. Tuttavia, se un programma più complesso allocasse molta memoria in un ciclo e la mantenesse per molto tempo, il programma utilizzerebbe più memoria di quanto necessario e potrebbe sovraccaricare il sistema, facendogli esaurire la memoria disponibile.

Creare cicli di riferimento non è facilmente fatto, ma non è neanche impossibile. Se hai valori RefCell<T> che contengono valori Rc<T> o combinazioni nidificate simili di tipi con mutabilità interna e conteggio dei riferimenti, devi assicurarti di non creare cicli; non puoi fare affidamento su Rust per coglierli. Creare un ciclo di riferimento sarebbe un bug logico nel tuo programma che dovresti minimizzare usando test automatizzati, revisioni del codice e altre pratiche di sviluppo del software.

Un’altra soluzione per evitare i cicli di riferimento è riorganizzare le tue strutture dati in modo che alcuni riferimenti esprimano la proprietà e alcuni riferimenti no. Di conseguenza, puoi avere cicli composti da alcune relazioni di proprietà e alcune relazioni non di proprietà, e solo le relazioni di proprietà influenzano se un valore può essere eliminato. Nel Listing 15-25, vogliamo sempre che le varianti Cons possiedano la loro lista, quindi non è possibile riorganizzare la struttura dei dati. Vediamo un esempio usando grafici composti da nodi genitori e nodi figli per capire quando le relazioni non di proprietà sono un modo appropriato per prevenire i cicli di riferimento.

Prevenire i Cicli di Riferimento: Trasformare un Rc<T> in un Weak<T>

Finora abbiamo dimostrato che chiamare Rc::clone aumenta il strong_count di un’istanza Rc<T>, e un’istanza Rc<T> viene liberata solo se il suo strong_count è 0. Puoi anche creare un riferimento debole al valore all’interno di un’istanza Rc<T> chiamando Rc::downgrade e passando un riferimento all’Rc<T>. I riferimenti forti sono come puoi condividere la proprietà di un’istanza Rc<T>. I riferimenti deboli non esprimono una relazione di proprietà, e il loro conteggio non influisce sulla pulizia di un’istanza Rc<T>. Non causeranno un ciclo di riferimento perché qualsiasi ciclo che coinvolge alcuni riferimenti deboli verrà interrotto una volta che il conteggio dei riferimenti forti dei valori coinvolti è 0.

Quando chiami Rc::downgrade, ottieni un puntatore intelligente di tipo Weak<T>. Invece di aumentare il strong_count nell’istanza Rc<T> di 1, chiamare Rc::downgrade aumenta il weak_count di 1. Il tipo Rc<T> utilizza weak_count per tenere traccia di quante referenze Weak<T> esistono, simile a strong_count. La differenza è che weak_count non deve essere 0 affinché l’istanza Rc<T> venga liberata.

Poiché il valore a cui si riferiscono i Weak<T> potrebbe essere stato eliminato, per fare qualcosa con il valore a cui punta un Weak<T>, devi assicurarti che il valore esista ancora. Fai questo chiamando il metodo upgrade su un’istanza Weak<T>, che restituirà un Option<Rc<T>>. Otterrai un risultato di Some se il valore Rc<T> non è ancora stato eliminato e un risultato di None se il valore Rc<T> è stato eliminato. Poiché upgrade restituisce un Option<Rc<T>>, Rust si assicurerà che il caso Some e il caso None siano gestiti, e non ci sarà un puntatore non valido.

Come esempio, anziché utilizzare una lista i cui elementi conoscono solo l’elemento successivo, creeremo un albero i cui elementi conoscono i loro figli e i loro genitori. Creare una Struttura Dati ad Albero: un Nodo con Nodi Figlio

Per iniziare, costruiremo un albero con nodi che conoscono i loro nodi figlio. Creeremo una struttura chiamata Node che contiene il proprio valore i32 oltre a riferimenti ai suoi nodi figlio:

rust

Filename: src/main.rs use std::cell::RefCell;
use std::rc::Rc;

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

Vogliamo che un Node possieda i suoi figli, e vogliamo condividere quella proprietà con le variabili in modo da poter accedere direttamente a ogni Node nell’albero. Per fare questo, definiamo gli elementi Vec<T> come valori di tipo Rc<Node>. Vogliamo anche modificare quali nodi sono figli di un altro nodo, quindi abbiamo un RefCell<T> in children attorno a Vec<Rc<Node>>.

Successivamente, useremo la nostra definizione di struttura e creeremo un’istanza Node chiamata leaf con il valore 3 e nessun figlio e un’altra istanza chiamata branch con il valore 5 e leaf come uno dei suoi figli, come mostrato nel Listing 15-27:

rust

Filename: src/main.rs 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: Creazione di un nodo leaf senza figli e di un nodo branch con leaf come uno dei suoi figli

Cloniamo l’Rc<Node> in leaf e lo memorizziamo in branch, il che significa che il Node in leaf ha ora due proprietari: leaf e branch. Possiamo passare da branch a leaf tramite branch.children, ma non c’è modo di passare da leaf a branch. Il motivo è che leaf non ha alcun riferimento a branch e non sa che sono correlati. Vogliamo che leaf sappia che branch è il suo genitore. Lo faremo prossimamente. Aggiunta di un Riferimento da un Figlio al Suo Genitore

Per far sì che il nodo figlio sia consapevole del suo genitore, dobbiamo aggiungere un campo parent alla nostra definizione di struttura Node. Il problema è nel decidere quale tipo dovrebbe essere il genitore. Sappiamo che non può contenere un Rc<T>, perché ciò creerebbe un ciclo di riferimento con leaf.parent che punta a branch e branch.children che punta a leaf, il che farebbe sì che i loro valori di strong_count non fossero mai 0.

Pensando alle relazioni in un altro modo, un nodo genitore dovrebbe possedere i suoi nodi figlio: se un nodo genitore viene eliminato, i suoi nodi figlio dovrebbero essere eliminati anche. Tuttavia, un figlio non dovrebbe possedere il suo genitore: se eliminiamo un nodo figlio, il genitore dovrebbe comunque esistere. Questo è un caso per i riferimenti deboli!

Quindi anziché Rc<T>, faremo in modo che il tipo di parent utilizzi Weak<T>, nello specifico un RefCell<Weak<Node>>. Ora la nostra definizione di struttura Node appare così:

rust

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>>>,
}

Un nodo sarà in grado di fare riferimento al suo nodo genitore ma non possiede il suo genitore. Nel Listing 15-28, aggiorniamo main per utilizzare questa nuova definizione in modo che il nodo leaf abbia un modo per fare riferimento al suo genitore, branch:

rust

Filename: src/main.rs 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: Un nodo leaf con un riferimento debole al nodo genitore branch

La creazione del nodo leaf appare simile al Listing 15-27 con l’eccezione del campo parent: leaf inizia senza un genitore, quindi creiamo una nuova istanza di riferimento debole Weak<Node> vuota.

A questo punto, quando cerchiamo di ottenere un riferimento al genitore di leaf utilizzando il metodo upgrade, otteniamo un valore None. Vediamo questo nell’output del primo statement println!:

leaf parent = None

Quando creiamo il nodo branch, avrà anche un nuovo riferimento debole Weak<Node> nel campo genitore, perché branch non ha un nodo genitore. Abbiamo ancora leaf come uno dei figli di branch. Una volta ottenuta l’istanza Node in branch, possiamo modificare leaf per dargli un riferimento debole Weak<Node> al suo genitore. Utilizziamo il metodo borrow_mut su RefCell<Weak<Node>> nel campo genitore di leaf, e poi usiamo la funzione Rc::downgrade per creare un riferimento debole Weak<Node> a branch dall’Rc<Node> in branch.

Quando stampiamo di nuovo il genitore di leaf, questa volta otterremo un Some che contiene branch: ora leaf può accedere al suo genitore! Quando stampiamo leaf, evitiamo anche il ciclo che alla fine ha causato un’overflow dello stack come avevamo nel Listing 15-26; i riferimenti Weak<Node> vengono stampati come (Weak):

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

La mancanza di un output infinito indica che questo codice non ha creato un ciclo di riferimento. Possiamo anche dirlo guardando i valori che otteniamo chiamando Rc::strong_count e Rc::weak_count. Visualizzazione dei Cambiamenti di strong_count e weak_count

Vediamo come cambiano i valori di strong_count e weak_count delle istanze Rc<Node> creando un nuovo scope interno e spostando la creazione di branch in quel scope. In questo modo, possiamo vedere cosa succede quando branch viene creato e poi eliminato quando esce dallo scope. Le modifiche sono mostrate nel Listing 15-29:

rust

Filename: src/main.rs 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: Creazione di branch in uno scope interno e esame dei conteggi dei riferimenti strong e weak

Dopo che leaf è stato creato, il suo Rc<Node> ha un conteggio strong di 1 e un conteggio weak di 0. Nello scope interno, creiamo branch e lo associamo a leaf, momento in cui quando stampiamo i conteggi, il Rc<Node> in branch avrà un conteggio strong di 1 e un conteggio weak di 1 (per leaf.parent che punta a branch con un Weak<Node>). Quando stampiamo i conteggi in leaf, vedremo che avrà un conteggio strong di 2, perché branch ora ha un clone dell’Rc<Node> di leaf memorizzato in branch.children, ma avrà ancora un conteggio weak di 0.

Quando lo scope interno termina, branch esce dallo scope e il conteggio strong del Rc<Node> diminuisce a 0, quindi il suo Node viene eliminato. Il conteggio weak di 1 da leaf.parent non ha alcun peso sulla eliminazione o meno di Node, quindi non otteniamo perdite di memoria!

Se proviamo ad accedere al genitore di leaf dopo la fine dello scope, otterremo nuovamente None. Alla fine del programma, il Rc<Node> in leaf ha un conteggio strong di 1 e un conteggio weak di 0, perché la variabile leaf è ora l’unico riferimento all’Rc<Node> nuovamente.

Tutta la logica che gestisce i conteggi e l’eliminazione dei valori è incorporata in Rc<T> e Weak<T> e nelle loro implementazioni del tratto Drop. Specificando che la relazione da un figlio al suo genitore dovrebbe essere un riferimento Weak<T> nella definizione di Node, sei in grado di fare in modo che i nodi genitori puntino ai nodi figlio e viceversa senza creare un ciclo di riferimento e perdite di memoria.

Riepilogo

Questo capitolo ha coperto come utilizzare i puntatori intelligenti per fare garanzie e scambi diversi da quelli che Rust fa per impostazione predefinita con i riferimenti regolari. Il tipo Box<T> ha una dimensione conosciuta e punta a dati allocati nell’heap. Il tipo Rc<T> tiene traccia del numero di riferimenti ai dati nell’heap in modo che i dati possano avere più proprietari. Il tipo RefCell<T> con la sua mutabilità interna ci dà un tipo che possiamo usare quando abbiamo bisogno di un tipo immutabile ma dobbiamo cambiare un valore interno di quel tipo; impone anche le regole di prestito a tempo di esecuzione anziché a tempo di compilazione.

Sono state discusse anche i tratti Deref e Drop, che abilitano molte delle funzionalità dei puntatori intelligenti. Abbiamo esplorato i cicli di riferimento che possono causare perdite di memoria e come prevenirli usando Weak<T>.

Se questo capitolo ha suscitato il tuo interesse e vuoi implementare i tuoi puntatori intelligenti, dai un’occhiata a “The Rustonomicon” per ulteriori informazioni utili.

Successivamente, parleremo di concorrenza in Rust. Imparerai anche su alcuni nuovi puntatori intelligenti.

1 Comment

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *