Ciclurile de referințe pot provoca scurgeri de memorie
Garanțiile oferite de Rust în privința securității memoriei fac dificilă, dar nu imposibilă, crearea accidentală a memoriei ce nu este curățată niciodată (ceea ce se numește scurgere de memorie). Rust nu garantează prevenirea totală a scurgerilor de memorie, deci scurgerile de memorie sunt considerate sigure din punct de vedere al securității memoriei în Rust. Vedem că Rust admite scurgerile de memorie când folosim Rc<T>
și RefCell<T>
: este posibil să construim referințe care formează un ciclu, unde elementele se referă reciproc. Acest proces determină scurgeri de memorie pentru că numărul de referințe pentru fiecare element din ciclu nu va scădea vreodată la zero, și în consecință, valorile nu vor fi niciodată eliberate.
Formarea unui ciclu de referințe
Să vedem cum se poate forma un ciclu de referințe și cum să îl evităm, începând cu definiția enum-ului List
și cu metoda tail
din Listarea 15-25:
Numele fișierului: 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() {}
Listarea 15-25: Definiție pentru o listă de tip cons care include un RefCell<T>
ce ne permite să modificăm la ce anume se referă o variantă Cons
Folosim o variantă modificată a definiției List
prezentată în Listarea 15-5. Cel de-al doilea element în varianta Cons
este acum RefCell<Rc<List>>
, astfel că, în loc să modificăm valoarea i32
cum am făcut în Listarea 15-24, acum dorim să putem modifica valoarea de tip List
la care se referă varianta Cons
. Am inclus și metoda tail
pentru a facilita accesul la al doilea element atunci când avem de-a face cu o variantă Cons
.
În Listarea 15-26, introducem funcția main
care utilizează definițiile din Listarea 15-25. Codul creat generează o listă în variabila a
și o altă listă în b
care face referire la lista din a
. Ulterior, modificăm lista din a
astfel încât să indice către b
, formând astfel un ciclu de referințe. Folosim instrucțiuni println!
pentru a ilustra numărul de referințe la diferite momente ale acestui proces.
Numele fișierului: 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()); }
Listarea 15-26: Construirea unui ciclu de referințe între două valori List
ce se referă reciproc
Creăm o instanță Rc<List>
ce conține o valoare de tip List
în variabila a
cu o listă inițială de 5, Nil
. Apoi, creăm o nouă instanță Rc<List>
care păstrează o altă valoare List
în variabila b
, aceasta având valoarea 10 și făcând referire la lista din a
.
Modificăm a
astfel încât acum să indice spre b
în loc de Nil
, creând astfel un ciclu. Realizăm aceasta prin folosirea metodei tail
pentru a accesa o referință la RefCell<Rc<List>>
din a
, pe care o salvăm în variabila link
. Apoi aplicăm metoda borrow_mut
pe RefCell<Rc<List>>
pentru a schimba valoarea dintr-un Rc<List>
ce conține un Nil
în Rc<List>
referențiat de b
.
La rularea acestui cod, lăsând ultimul println!
comentat deocamdată, vom primi următorul afișaj:
$ cargo run
Compiling 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
Contorul de referințe pentru instanțele Rc<List>
din a
și b
este de 2 după ce am modificat lista din a
pentru a pointa acum către b
. La terminarea funcției main
, Rust renunță la variabila b
, reducând contorul de referințe al instanței b
Rc<List>
de la 2 la 1. Memoria ocupată de Rc<List>
în heap nu va fi eliberată în acest punct, deoarece contorul său de referințe este 1, nu 0. Apoi Rust renunță la a
, diminuând contorul de referințe al instanței a
Rc<List>
și el de la 2 la 1. Nici memoria acestei instanțe nu poate fi eliberată, deoarece instanța Rc<List>
rămâne cu referință la ea. Astfel, memoria alocată acestei liste va rămâne colectată pe durată nelimitată. Pentru a vizualiza acest ciclu de referințe, am creat o diagramă în Figura 15-4.
Figura 15-4: Un ciclu de referințe în care listele a
și b
se referă reciproc
Dacă vei decomenta ultimul println!
și executa programul din nou, Rust va încerca să afișeze acest ciclu cu a
pointând către b
, care la rândul său pointează către a
și așa mai departe, până când va supraîncărca stiva.
Comparativ cu un program din lumea reală, consecințele creării unui ciclu de referințe în acest exemplu nu sunt foarte severe: imediat după ce creăm ciclul de referințe, programul se încheie. Totuși, în cazul unui program mai complex care alocă o cantitate mare de memorie într-un ciclu și o menține pentru mult timp, acesta ar consuma mai multă memorie decât necesar și ar putea suprasolicita sistemul, ceea ce ar duce la epuizarea memoriei disponibile.
Crearea accidentală a ciclurilor de referințe nu este un proces simplu, dar nu este nici imposibil. Dacă utilizezi valori de tip RefCell<T>
ce conțin valori Rc<T>
sau combinații tipuri similare cu mutabilitate interioară și numărare de referințe, trebuie să te asigura că nu formezi cicluri; nu te poți baza pe Rust pentru a identifica aceste probleme. Crearea unui ciclu de referințe constituie o eroare de logică în programul tău și ar trebui să utilizezi teste automate, recenzii de cod și alte metodologii de dezvoltare software pentru a le reduce la minim.
O altă soluție pentru evitarea ciclurilor de referințe este reorganizarea structurilor de date astfel încât unele referințe să reprezinte posesiunea, iar altele nu. În consecință, poți avea cicluri constituite din relații de posesiune și relații care nu implică posesiunea, pe când doar relațiile de posesiune influențează posibilitatea de a elibera o valoare. În Listarea 15-25, dorim ca variantele Cons
să dețină întotdeauna listele lor, astfel încât reorganizarea structurii de date nu este posibilă. Să examinăm un exemplu folosind grafuri alcătuite din noduri părinte și noduri copil pentru a înțelege când relațiile non-posesive sunt o modalitate adecvată pentru prevenirea ciclurilor de referințe.
Prevenirea ciclurilor de referințe: Convertirea unui Rc<T>
în Weak<T>
Până acum, am arătat că invocarea Rc::clone
mărește strong_count
al unei instanțe Rc<T>
, iar o instanță Rc<T>
este eliberată din memorie doar când strong_count
este 0. Poți crea de asemenea o referință slabă la valoarea conținută de o instanță Rc<T>
apelând Rc::downgrade
și furnizând o referință la Rc<T>
. Referințele puternice reprezintă o metodă de a partaja posesiunea unei instanțe Rc<T>
, în timp ce referințele slabe nu reflectă o relație de posesiune și prezența lor nu influențează momentul în care instanța Rc<T>
este eliberată. Referințele slabe nu conduc la cicluri de referință deoarece orice ciclu care include referințe slabe se desface atunci când numărul referințelor puternice ale valorilor implicate ajunge la 0.
La apelarea Rc::downgrade
, se obține un pointer inteligent de tip Weak<T>
. Pe lângă creșterea strong_count
în instanța Rc<T>
cu 1 cum se întâmplă prin Rc::clone
, invocarea Rc::downgrade
crește weak_count
cu 1. Rc<T>
folosește weak_count
pentru a contoriza câte referințe Weak<T>
active există, asemeni lui strong_count
pentru referințele puternice. O diferență majoră este că weak_count
nu este nevoit să fie 0 pentru ca instanța Rc<T>
să fie eliberată.
Din moment ce valoarea către care Weak<T>
referă s-ar putea să fie deja eliberată, pentru a interacționa cu valoarea indicată de un Weak<T>
e necesar să verifici dacă valoarea încă există. Acesta se face apelând metoda upgrade
la o instanță de Weak<T>
, care va returna Option<Rc<T>>
. Dacă valoarea Rc<T>
nu a fost eliberată, rezultatul va fi Some
, în timp ce dacă valoarea Rc<T>
a fost eliberată vei obține None
. Cu upgrade
returnând un Option<Rc<T>>
, Rust se asigură că cazurile Some
și None
sunt adecvat gestionate, evitând existența unui pointer invalid.
Un exemplu ar fi, în loc să utilizăm o listă în care fiecare element știe doar despre următorul element, să construim un arbore unde elementele cunosc atât elementele succesoare care sunt copiii lor cât și elementul anterior care este părintele lor.
Crearea unei structuri de date de tip arbore: un Node
cu noduri copil
Pentru început, vom construi un arbore în care nodurile cunosc nodurile lor copil. Vom crea un struct numit Node
care conține propria valoare i32
și referințe către valorile Node
ale copiilor săi:
Numele fișierului: 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)]), }); }
Dorim ca un Node
să își dețină propriii copii și mai dorim să partajăm această posesiune cu variabile pentru a putea accesa direct fiecare Node
din arbore. Pentru a realiza acest lucru, definim elementele Vec<T>
să fie de tip Rc<Node>
. În plus, dorim să putem modifica care noduri sunt copiii altui nod, astfel încât avem un RefCell<T>
peste Vec<Rc<Node>>
în children
.
În continuare, vom folosi definiția noastră de structură pentru a crea o instanță de tip Node
denumită leaf
cu valoarea 3 și fără copii, și altă instanță de tip Node
numită branch
cu valoarea 5 și cu leaf
ca unul dintre copiii săi, așa cum se arată în Listarea 15-27:
Numele fișierului: 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)]), }); }
Listarea 15-27: Crearea unui nod leaf
fără copii și a unui nod branch
cu leaf
ca unul dintre copiii săi
Facem o clonă a Rc<Node>
din leaf
și o stocăm în branch
, astfel încât nodul din leaf
acum are doi proprietari: leaf
și branch
. Putem naviga de la branch
la leaf
prin branch.children
, dar nu există o cale de a merge de la leaf
la branch
. Motivul este că leaf
nu are o referință către branch
și nu cunoaște relația dintre ele. Vrem ca leaf
să cunoască că branch
este părintele său. Asta vom realiza în pasul următor.
Adăugarea unei referințe de la un nod copil la părintele său
Pentru a-l face pe nodul copil conștient de părintele său, trebuie să adăugăm un câmp parent
în definiția noastră a structurii Node
. Provocarea este să decidem ce tip ar trebui să fie parent
. Știm că nu poate fi Rc<T>
, deoarece acesta ar crea un ciclu de referințe cu leaf.parent
care arată către branch
și branch.children
care arată înapoi către leaf
, fapt ce ar cauza ca valorile lor de strong_count
să nu ajungă niciodată la 0.
Privind relațiile dintr-o altă perspectivă, un nod părinte ar trebui să își dețină copiii: dacă un nod părinte este șters din memorie, nodurile sale copil ar trebui să fie de asemenea șterse. Cu toate acestea, un copil nu ar trebui să-și dețină părintele: dacă ștergem un nod copil, părintele ar trebui să rămână în existență. Acesta este exact un caz pentru referințele slabe!
Prin urmare, în loc de Rc<T>
, vom folosi Weak<T>
pentru tipul câmpului parent
, mai exact RefCell<Weak<Node>>
. Cu această schimbare, definiția structurii noastre Node
acum arată în felul următor:
Numele fișierului: 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()); }
Astfel, un nod va putea să facă referire la nodul părinte fără să îl dețină. În Listarea 15-28, actualizăm funcția main
pentru a folosi această nouă definiție, astfel încât nodul leaf
să aibă posibilitatea de a se referi la părintele său 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()); }
Listarea 15-28: Un nod leaf
cu o referință slabă către părintele său branch
Procesul de creare a nodului leaf
este similar cu cel din Listarea 15-27, cu excepția câmpului parent
: leaf
începe fără un părinte, deci creăm o instanță nouă și goală a referinței Weak<Node>
.
Aici, când încercăm să obținem o referință la părintele lui leaf
folosind metoda upgrade
, primim o valoare None
. Acest lucru ni se arată în afișajul de la prima instrucțiune println!
:
leaf parent = None
Atunci când construim nodul branch
, acesta va avea de asemenea o nouă referință Weak<Node>
în câmpul parent
, fiindcă branch
nu dispune de un nod părinte. Totuși, păstrăm leaf
ca fiind unul dintre copiii lui branch
. După ce obținem instanța Node
în branch
, putem modifica leaf
pentru a-i atribui o referință Weak<Node>
către părintele său. Apelăm metoda borrow_mut
asupra lui RefCell<Weak<Node>>
din câmpul parent
al leaf
și apoi folosim funcția Rc::downgrade
pentru a crea o referință Weak<Node>
către branch
plecând de la Rc<Node>
din branch.
După ce afișăm din nou părintele lui leaf
, de data aceasta vom primi o varianta Some
care include branch
: acum leaf
poate să-și acceseze părintele! În momentul în care afișăm leaf
, evităm și ciclul care a condus anterior la o supraîncărcare de stivă cum a avut loc în Listarea 15-26; referințele Weak<Node>
sunt afișate ca (Weak)
:
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })
Absența unei ieșiri infinite ne indică faptul că acest cod nu a generat un ciclu de referințe. Acest lucru poate fi de asemenea confirmat prin valorile obținute la apelarea funcțiilor Rc::strong_count
și Rc::weak_count
.
Vizualizarea schimbărilor în strong_count
și weak_count
Să vedem cum valorile strong_count
și weak_count
ale instanțelor Rc<Node>
se modifică prin crearea unui nou domeniu de vizibilitate intern și mutând inițializarea lui branch
în acest domeniu. Prin aceasta, putem observa ce se întâmplă când branch
este creat și apoi când este eliberat la ieșirea din domeniu. Modificările sunt ilustrate în Listarea 15-29:
Numele fișierului: 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), ); }
Listarea 15-29: Crearea branch
într-un domeniu de vizibilitate intern și analiza numărului de referințe puternice și slabe
După inițializarea lui leaf
, Rc<Node>
aferent are un strong_count
de 1 și un weak_count
de 0. În domeniul intern, creăm branch
și îl asociem cu leaf
, iar la acel punct, când printăm contoarele, Rc<Node>
din branch
va avea un strong_count
de 1 și un weak_count
de 1 (datorită lui leaf.parent
care indică spre branch
folosind Weak<Node>
). La printarea contoarelor pentru leaf
, vom observa că acesta va avea un strong_count
de 2, deoarece branch
deține acum un clone al Rc<Node>
de la leaf
în branch.children
, dar weak_count
va rămâne la 0.
La finalul domeniului intern, branch
părăsește domeniul de vizibilitate, iar strong_count
pentru Rc<Node>
scade la 0, ceea ce duce la eliberarea lui Node
. Valoarea lui weak_count
de 1 provenind de la leaf.parent
nu afectează eliberarea lui Node
, așadar nu apar scurgeri de memorie!
Dacă încercăm să accesăm părintele lui leaf
după ce domeniul intern se încheie, vom primi din nou None
. La finalul programului, Rc<Node>
în leaf
va avea un strong_count
de 1 și un weak_count
de 0, fiindcă variabila leaf
este din nou unica referință către Rc<Node>
.
Toată logica de gestionare a contoarelor și de eliberare a valorilor este inclusă în Rc<T>
și Weak<T>
, precum și în implementările acestora ale trăsăturii Drop
. Prin definirea relației dintre un nod copil și părintele acestuia ca fiind o referință Weak<T>
în cadrul definiției lui Node
, este posibil să ai noduri părinte care se referă către noduri copil și invers fără a genera cicluri de referințe și scurgeri de memorie.
Sumar
Am acoperit în acest capitol cum să exploatați pointerii inteligenți pentru a face unele garanții și compromisuri diferite comparativ cu cele implicite în Rust prin referințele standard. Tipul Box<T>
are o dimensiune definită și se referă la date aflate pe heap. Tipul Rc<T>
monitorizează numărul de referințe la datele pe heap, permițând astfel datelor să fie deținute de mai mulți proprietari. RefCell<T>
, prin mutabilitatea sa internă, ne oferă un tip care este util atunci când avem nevoie de un obiect imutabil, dar dorim să schimbăm o valoare internă a acestuia; totodată, se asigură că regulile de împrumut sunt respectate în timpul execuției, nu în timpul compilării.
Au fost, de asemenea, discutate trăsăturile Deref
și Drop
, care fac posibilă o mare parte din funcționalitățile pointerilor inteligenți. Am explorat ciclurile de referință care pot provoca scurgeri de memorie și cum să le prevenim prin utilizarea Weak<T>
.
Dacă te-a captivat acest capitol și ai vrea să creezi proprii tăi pointeri inteligenți, te încurajăm să consulți „The Rustonomicon” pentru mai multe informații de valoare.
În capitolul următor, vom explora programarea concurentă în Rust. Și chiar vei avea ocazia să înveți despre careva noi tipuri de pointeri inteligenți.