Reference Cycles Bisa Membocorkan Memori (Memory Leak)
Jaminan keamanan memori (memory safety guarantees) di Rust membikin hal itu
jadi sulit, tapi bukannya mustahil, buat secara tidak sengaja membikin memori
yang tidak akan pernah dibersihkan (dikenal sebagai memory leak atau
kebocoran memori). Mencegah memory leaks secara total bukanlah salah satu
jaminan yang diberikan Rust, yang berarti memory leaks itu sifatnya aman-memori
(memory safe) di Rust. Kita bisa melihat kalau Rust mengizinkan memory leaks
dengan memakai Rc<T> dan RefCell<T>: sangat mungkin buat membikin referensi
di mana item-itemnya merujuk satu sama lain dalam sebuah cycle (siklus).
Ini membikin memory leaks karena jumlah referensi (reference count) dari
setiap item di dalam cycle tidak akan pernah mencapai 0, dan nilainya
tidak akan pernah di-drop.
Membikin Sebuah Reference Cycle
Mari kita lihat gimana sebuah reference cycle bisa terjadi dan gimana cara
mencegahnya, dimulai dengan definisi dari enum List dan method tail
di Listing 15-25.
// ANCHOR: here
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,
}
}
}
// ANCHOR_END: here
fn main() {}
RefCell<T> supaya kita bisa memodifikasi apa yang ditunjuk oleh varian ConsKita memakai variasi lain dari definisi List dari Listing 15-5. Elemen
kedua di varian Cons sekarang adalah RefCell<Rc<List>>, yang berarti
bahwa ketimbang cuma punya kemampuan buat memodifikasi nilai i32 seperti
yang kita lakuin di Listing 15-24, kita sekarang mau memodifikasi nilai List
yang ditunjuk oleh sebuah varian Cons. Kita juga menambahkan method tail
buat memudahkan kita mengakses item kedua kalau kita punya sebuah varian Cons.
Di Listing 15-26, kita menambahkan fungsi main yang memakai definisi di
Listing 15-25. Kode ini membikin sebuah list di a dan sebuah list di b
yang menunjuk ke list di a. Terus dia memodifikasi list di a buat menunjuk
ke b, sehingga membikin sebuah reference cycle. Ada statements println!
di sepanjang jalan buat menunjukkan berapa jumlah referensi (reference counts)
di berbagai titik dalam proses ini.
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 yang saling menunjuk satu sama lainKita membikin instance Rc<List> yang memegang sebuah nilai List di variabel
a dengan list awal berupa 5, Nil. Terus kita membikin instance Rc<List>
yang memegang nilai List lainnya di variabel b yang berisi nilai 10
dan menunjuk ke list di a.
Kita memodifikasi a agar dia menunjuk ke b ketimbang ke Nil, sehingga
membikin sebuah cycle. Kita melakukan itu dengan memakai method tail buat
mendapatkan sebuah referensi ke RefCell<Rc<List>> di a, yang kemudian kita
taruh di variabel link. Lalu kita memakai method borrow_mut pada
RefCell<Rc<List>> tersebut buat mengubah nilai internalnya dari sebuah
Rc<List> yang memegang nilai Nil menjadi Rc<List> yang ada di b.
Saat kita menjalankan kode ini, dengan membiarkan println! terakhir
tetap dikomentari (commented out) buat saat ini, kita bakal dapat output ini:
$ 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
Jumlah referensi (reference count) dari instance Rc<List> di a maupun b
adalah 2 setelah kita ngubah list di a agar menunjuk ke b. Di akhir dari
main, Rust men-drop variabel b, yang mana menurunkan jumlah referensi
dari instance Rc<List> si b dari 2 menjadi 1. Memori yang dimiliki Rc<List>
di heap tidak bakal di-drop di titik ini karena jumlah referensinya itu 1,
bukannya 0. Terus Rust men-drop a, yang mana menurunkan jumlah referensi dari
instance Rc<List> si a dari 2 menjadi 1 juga. Memori dari instance ini
juga tidak bisa di-drop, karena instance Rc<List> yang satunya lagi masih
merujuk ke dia. Memori yang dialokasikan ke list tersebut bakal terus tersisa
dan tidak dibersihkan selamanya. Buat memvisualisasikan reference cycle ini,
kita sudah membikin diagram di Gambar 15-4.
Gambar 15-4: Sebuah reference cycle dari list a dan
b yang saling menunjuk satu sama lain
Kalau kita menghilangkan komentar (uncomment) pada println! yang terakhir lalu
menjalankan programnya, Rust bakal mencoba mencetak cycle ini dengan a
menunjuk ke b menunjuk ke a dan seterusnya sampai dia mengalami stack overflow.
Dibandingkan dengan program di dunia nyata, konsekuensi dari membikin reference cycle di contoh ini tidak terlalu fatal: sesaat setelah kita membikin reference cycle tersebut, programnya berakhir. Namun, kalau sebuah program yang lebih kompleks mengalokasikan banyak memori di dalam sebuah cycle lalu memegangnya untuk waktu yang lama, program tersebut bakal memakai lebih banyak memori daripada yang dia butuhkan dan bisa bikin sistem kewalahan, yang berujung pada kehabisan memori (out of memory).
Membikin reference cycles itu memang tidak mudah dilakukan, tapi itu juga
bukanlah hal yang mustahil. Kalau kita punya nilai RefCell<T> yang
mengandung nilai Rc<T> atau kombinasi bersarang yang mirip dari tipe-tipe yang
punya interior mutability dan reference counting, kita harus memastikan
kalau kita tidak membikin cycles; kita tidak bisa ngandelin Rust buat
menangkap hal tersebut. Membikin sebuah reference cycle adalah sebuah logic
bug (kutu logika) di program kita yang harusnya diminimalisir dengan memakai
automated tests (pengujian otomatis), code reviews (tinjauan kode), dan
praktik pengembangan software lainnya.
Solusi lain buat menghindari reference cycles adalah dengan mengatur ulang
struktur data kita sedemikian rupa sehingga beberapa referensi mengekspresikan
kepemilikan (ownership) dan referensi lainnya tidak. Sebagai hasilnya, kita
bisa punya cycles yang dibikin dari beberapa hubungan kepemilikan dan beberapa
hubungan non-kepemilikan, dan cuma hubungan kepemilikan lah yang memengaruhi
apakah suatu nilai bisa di-drop atau tidak. Di Listing 15-25, kita selalu mau
varian Cons buat memiliki list mereka, jadi mengatur ulang struktur datanya
itu tidak memungkinkan. Mari kita lihat contoh yang memakai graphs (graf) yang
dibikin dari parent nodes (simpul induk) dan child nodes (simpul anak)
buat melihat kapan hubungan non-kepemilikan menjadi cara yang pas buat
mencegah reference cycles.
Mencegah Reference Cycles Memakai Weak<T>
Sejauh ini, kita sudah mendemonstrasikan kalau memanggil Rc::clone bakal
menaikkan nilai strong_count dari sebuah instance Rc<T>, dan sebuah instance
Rc<T> cuma bakal dibersihkan kalau nilai strong_count-nya 0. Kita juga bisa
membikin sebuah weak reference (referensi lemah) ke sebuah nilai yang ada
di dalam instance Rc<T> dengan memanggil Rc::downgrade dan meneruskan sebuah
referensi ke Rc<T> tersebut. Strong references (referensi kuat) adalah cara
gimana kita bisa berbagi kepemilikan dari sebuah instance Rc<T>. Weak references
tidak mengekspresikan hubungan kepemilikan, dan jumlah mereka (count) tidak
memengaruhi kapan sebuah instance Rc<T> dibersihkan. Mereka tidak bakal bikin
sebuah reference cycle karena cycle apa pun yang melibatkan beberapa weak
references bakal terputus begitu jumlah strong reference dari nilai-nilai yang
terlibat mencapai 0.
Pas kita memanggil Rc::downgrade, kita bakal dapat sebuah smart pointer
bertipe Weak<T>. Alih-alih menaikkan strong_count di instance Rc<T> sebanyak
1, memanggil Rc::downgrade menaikkan weak_count sebanyak 1. Tipe Rc<T> memakai
weak_count buat melacak berapa banyak referensi Weak<T> yang ada, mirip
kayak strong_count. Bedanya adalah weak_count tidak harus 0 supaya instance
Rc<T>-nya bisa dibersihkan.
Karena nilai yang ditunjuk sama Weak<T> mungkin sudah di-drop, untuk melakukan
apa pun dengan nilai yang ditunjuk sama Weak<T> kita harus memastikan kalau
nilainya masih eksis. Lakukan ini dengan memanggil method upgrade pada sebuah
instance Weak<T>, yang mana bakal mengembalikan sebuah Option<Rc<T>>. Kita
bakal dapat hasil Some kalau nilai Rc<T> tersebut belum di-drop dan hasil
None kalau nilai Rc<T> tersebut sudah di-drop. Karena upgrade mengembalikan
sebuah Option<Rc<T>>, Rust bakal memastikan kalau kasus Some maupun kasus None
udah ditangani, dan tidak bakal ada yang namanya pointer yang tidak valid.
Sebagai contoh, ketimbang memakai sebuah list yang item-itemnya cuma tahu soal item selanjutnya aja, kita bakal membikin sebuah struktur tree (pohon) yang item-itemnya tahu soal children items (item anak) mereka dan parent items (item induk) mereka.
Membikin Struktur Data Tree: Sebuah Node dengan Child Nodes
Sebagai awalan, kita bakal ngebangun sebuah tree yang terdiri dari nodes (simpul)
yang tahu soal child nodes mereka. Kita bakal membikin sebuah struct bernama Node
yang memegang nilai i32-nya sendiri dan juga referensi ke nilai-nilai Node dari
children-nya:
Nama file: 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)]),
});
}
Kita pengen supaya sebuah Node memiliki children-nya, dan kita mau berbagi
kepemilikan tersebut dengan berbagai variabel sehingga kita bisa mengakses
setiap Node di tree tersebut secara langsung. Buat melakukannya, kita
mendefinisikan item-item Vec<T> agar berupa nilai-nilai bertipe Rc<Node>.
Kita juga mau mengubah nodes mana saja yang merupakan children dari node lain,
jadi kita membungkus Vec<Rc<Node>> tersebut di dalam sebuah RefCell<T> di
field children.
Selanjutnya, kita bakal memakai definisi struct kita buat membikin satu instance
Node bernama leaf (daun) dengan nilai 3 dan tanpa children, serta instance
lain bernama branch (cabang) dengan nilai 5 dan leaf sebagai salah satu
children-nya, seperti yang ditunjukkan di Listing 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)]),
});
}
Kita meng-clone Rc<Node> yang ada di leaf dan menyimpannya di dalam
branch, yang artinya Node di leaf sekarang punya dua pemilik: leaf
dan branch. Kita bisa pindah dari branch ke leaf melalui branch.children,
tapi tidak ada cara buat pindah dari leaf ke branch. Alasannya adalah karena
leaf tidak punya referensi ke branch dan tidak tahu kalau mereka itu
berhubungan. Kita pengen supaya leaf tahu kalau branch itu adalah parent-nya
(induknya). Kita bakal melakukan hal itu selanjutnya.
Menambahkan Referensi dari Child ke Parent-nya
Buat membikin si child node sadar akan parent-nya, kita perlu menambahkan
sebuah field parent ke definisi struct Node kita. Masalahnya adalah
menentukan apa seharusnya tipe dari parent ini. Kita tahu kalau dia tidak
boleh berisi Rc<T>, karena itu bakal membikin sebuah reference cycle dengan
leaf.parent yang menunjuk ke branch dan branch.children yang menunjuk ke
leaf, yang mana bakal membikin nilai strong_count mereka tidak akan
pernah mencapai 0.
Membayangkan hubungan ini dengan cara lain, sebuah parent node seharusnya memiliki children-nya: kalau sebuah parent node di-drop, child nodes-nya seharusnya ikut di-drop juga. Namun, sebuah child tidak seharusnya memiliki parent-nya: kalau kita men-drop sebuah child node, sang parent seharusnya tetap eksis. Ini adalah kasus yang tepat sekali buat memakai weak references!
Jadi ketimbang memakai Rc<T>, kita bakal membikin tipe dari parent tersebut
agar memakai Weak<T>, spesifiknya adalah RefCell<Weak<Node>>. Sekarang
definisi struct Node kita kelihatan kayak gini:
Nama file: 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());
}
Sebuah node sekarang bakal bisa merujuk ke parent node-nya tapi dia tidak
memiliki parent tersebut. Di Listing 15-28, kita meng-update main buat memakai
definisi baru ini sehingga node leaf bakal punya cara buat merujuk ke
parent-nya, yaitu 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());
}
branchMembikin node leaf ini kelihatan mirip sama yang di Listing 15-27 dengan
pengecualian di field parent: leaf awalnya tidak punya parent, jadi kita
membikin sebuah instance referensi Weak<Node> baru yang kosong.
Pada titik ini, saat kita mencoba buat mendapatkan referensi ke parent dari
leaf dengan memakai method upgrade, kita bakal dapat sebuah nilai None.
Kita bisa melihat ini di dalam output dari statement println! yang pertama:
leaf parent = None
Saat kita membikin node branch, dia juga bakal punya sebuah referensi
Weak<Node> baru di field parent-nya karena branch tidak punya parent
node. Kita tetap punya leaf sebagai salah satu dari children si branch.
Setelah kita mendapatkan instance Node di dalam branch, kita bisa
memodifikasi leaf buat ngasih dia sebuah referensi Weak<Node> ke
parent-nya. Kita memakai method borrow_mut pada RefCell<Weak<Node>> di
dalam field parent si leaf, dan terus kita pakai fungsi Rc::downgrade buat
membikin sebuah referensi Weak<Node> ke branch dari Rc<Node> yang ada
di dalam branch.
Pas kita mencetak parent dari leaf lagi, kali ini kita bakal dapat
sebuah varian Some yang memegang branch: sekarang leaf bisa mengakses
parent-nya! Pas kita mencetak leaf, kita juga terhindar dari cycle yang
pada akhirnya berujung pada stack overflow kayak yang terjadi di Listing 15-26;
referensi Weak<Node> tersebut cuma dicetak sebagai (Weak):
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })
Absennya (lack of) output yang tidak terhingga ini menandakan bahwa kode ini
tidak membikin sebuah reference cycle. Kita juga bisa mengetahuinya dengan melihat
ke nilai-nilai yang kita dapat dari memanggil Rc::strong_count dan
Rc::weak_count.
Memvisualisasikan Perubahan pada strong_count dan weak_count
Mari kita lihat gimana nilai strong_count dan weak_count dari instance-instance
Rc<Node> tersebut berubah dengan membikin sebuah inner scope (scope dalam) baru
lalu memindahkan pembuatan branch ke dalam scope tersebut. Dengan melakukan ini,
kita bisa melihat apa yang terjadi saat branch dibikin dan kemudian di-drop
pas dia keluar dari scope. Modifikasi ini ditunjukkan di Listing 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 di sebuah inner scope dan memeriksa jumlah (count) strong dan weak reference-nyaSetelah leaf dibikin, Rc<Node> miliknya punya strong count sebesar 1 dan
weak count sebesar 0. Di dalam inner scope, kita membikin branch dan
mengaitkannya (associate) dengan leaf, di mana pada titik ini kalau kita
mencetak jumlahnya, Rc<Node> di dalam branch bakal punya strong count 1
dan weak count 1 (karena leaf.parent menunjuk ke branch dengan
sebuah Weak<Node>). Pas kita mencetak jumlah (counts) yang ada di leaf,
kita bakal melihat kalau dia punya strong count sebesar 2 karena branch
sekarang punya clone dari Rc<Node> milik leaf yang disimpan di
branch.children, tapi dia tetap punya weak count sebesar 0.
Saat inner scope-nya berakhir, branch keluar dari scope dan strong count
dari Rc<Node>-nya turun menjadi 0, jadi Node-nya bakal di-drop. Weak count
sebesar 1 dari leaf.parent sama sekali tidak memengaruhi apakah si Node itu
bakal di-drop atau tidak, jadi kita tidak dapat memory leaks!
Kalau kita nyoba mengakses parent dari leaf setelah akhir dari scope itu,
kita bakal dapat None lagi. Di akhir dari program, Rc<Node> di dalam leaf
punya strong count sebesar 1 dan weak count sebesar 0 karena variabel leaf
sekarang menjadi satu-satunya referensi ke Rc<Node> tersebut lagi.
Semua logika yang mengelola jumlah referensi (counts) dan pen-drop-an nilai ini
dibangun langsung di dalam Rc<T> dan Weak<T> beserta implementasi mereka
pada trait Drop. Dengan secara spesifik menentukan kalau hubungan dari seorang
child ke parent-nya haruslah memakai referensi Weak<T> di dalam definisi
dari Node, kita bisa membikin parent nodes menunjuk ke child nodes dan
begitu juga sebaliknya tanpa membikin sebuah reference cycle dan memory leaks.
Ringkasan
Bab ini membahas gimana cara memakai smart pointers buat membikin berbagai jaminan
dan trade-offs (pertukaran) yang berbeda dari apa yang Rust lakukan secara
default dengan referensi biasa. Tipe Box<T> punya ukuran yang sudah pasti
diketahui dan menunjuk ke data yang dialokasikan di heap. Tipe Rc<T> melacak
jumlah referensi ke data yang ada di heap supaya data tersebut bisa punya
banyak pemilik. Tipe RefCell<T> dengan kemampuan interior mutability-nya
ngasih kita tipe yang bisa kita pakai pas kita butuh tipe yang immutable tapi
juga butuh buat ngubah nilai di dalamnya; ia juga menegakkan aturan borrowing
saat runtime bukannya saat compile time.
Kita juga udah ngebahas trait Deref dan Drop, yang memungkinkan berjalannya
banyak dari fungsionalitas smart pointers ini. Kita mengeksplorasi reference
cycles yang bisa menyebabkan memory leaks dan gimana cara mencegah mereka
memakai Weak<T>.
Kalau bab ini sudah membangkitkan rasa penasaran kita dan kita pengen mengimplementasikan smart pointers kita sendiri, silakan cek “The Rustonomicon” buat dapetin lebih banyak informasi yang berguna.
Berikutnya, kita bakal ngomongin soal konkurensi (concurrency) di Rust. Kita bahkan bakal mempelajari beberapa smart pointers baru lagi lho!