Referans Döngüleri Bellek Sızıntısına Yol Açabilir
Rust’ın bellek güvenliği garantileri, yanlışlıkla hiç temizlenmeyecek bellek
oluşturmayı zorlaştırır; ama imkânsız yapmaz. Buna bellek sızıntısı denir.
Bellek sızıntılarını tamamen önlemek Rust’ın garantileri arasında değildir; yani
Rust’ta bellek sızıntısı bellek açısından güvenlidir. Rc<T> ve RefCell<T>
kullanarak bunu görebiliriz: öğelerin birbirine döngü oluşturacak şekilde
referans verdiği yapılar kurmak mümkündür. Böyle durumda döngüdeki her öğenin
referans sayısı sıfıra düşmez ve değerler asla bırakılmaz.
Referans Döngüsü Oluşturmak
Bunun nasıl olabileceğine, Liste 15-25’teki Liste tanımı ve kuyruk metodu
ile bakalım.
use crate::Liste::{Bos, Dugum};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum Liste {
Dugum(i32, RefCell<Rc<Liste>>),
Bos,
}
impl Liste {
fn kuyruk(&self) -> Option<&RefCell<Rc<Liste>>> {
match self {
Dugum(_, oge) => Some(oge),
Bos => None,
}
}
}
fn main() {}
Dugum varyantinin gosterdigi seyi degistirebilmek icin RefCell<T> tutan kons liste tanimiBu, Liste 15-5’teki Liste tanımının başka bir varyasyonudur. Dugum
varyantının ikinci alanı artık RefCell<Rc<Liste>>; yani Liste 15-24’teki gibi
i32yi değil, Dugumün işaret ettiği Listeyi değiştirmek istiyoruz.
kuyruk metodu da ikinci öğeye erişmeyi kolaylaştırıyor.
Liste 15-26’da, bu tanımı kullanan maini ekliyoruz. Kod, a adlı bir liste ve
ona işaret eden b adlı başka bir liste oluşturuyor. Sonra ayı, Bos yerine
byi gösterecek biçimde değiştirerek referans döngüsü yaratıyor.
println! satırları süreç boyunca referans sayılarını gösteriyor.
use crate::Liste::{Bos, Dugum};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum Liste {
Dugum(i32, RefCell<Rc<Liste>>),
Bos,
}
impl Liste {
fn kuyruk(&self) -> Option<&RefCell<Rc<Liste>>> {
match self {
Dugum(_, oge) => Some(oge),
Bos => None,
}
}
}
fn main() {
let a = Rc::new(Dugum(5, RefCell::new(Rc::new(Bos))));
println!("a icin ilk rc sayisi = {}", Rc::strong_count(&a));
println!("a sonraki oge = {:?}", a.kuyruk());
let b = Rc::new(Dugum(10, RefCell::new(Rc::clone(&a))));
println!("b olustuktan sonra a rc sayisi = {}", Rc::strong_count(&a));
println!("b icin ilk rc sayisi = {}", Rc::strong_count(&b));
println!("b sonraki oge = {:?}", b.kuyruk());
if let Some(baglanti) = a.kuyruk() {
*baglanti.borrow_mut() = Rc::clone(&b);
}
println!("a degistikten sonra b rc sayisi = {}", Rc::strong_count(&b));
println!("a degistikten sonra a rc sayisi = {}", Rc::strong_count(&a));
// Bir dongu olustugunu gormek icin sonraki satirin yorumunu kaldirin;
// bu, yigin tasmasina yol acar.
// println!("a sonraki oge = {:?}", a.kuyruk());
}
Liste degeriyle referans dongusu olusturmaka değişkeninde başlangıçta 5, Bos listesini tutan bir Rc<Liste> kuruyoruz.
Sonra 10 değerini taşıyan ve ayı işaret eden bir başka Rc<Liste>yi b
değişkenine koyuyoruz.
Ardından ayı Bos yerine byi gösterecek şekilde değiştiriyoruz. Bunun için
a.kuyruk() ile RefCell<Rc<Liste>>ye referans alıp baglantiya koyuyor,
sonra borrow_mut ile içteki Rc<Liste>yi bye çeviriyoruz.
Son println!i şimdilik yorumlu bırakarak kodu çalıştırırsak şu çıktıyı alırız:
$ cargo run
Compiling kons-liste v0.1.0 (file:///projects/kons-liste)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/kons-liste`
a icin ilk rc sayisi = 1
a sonraki oge = Some(RefCell { value: Bos })
b olustuktan sonra a rc sayisi = 2
b icin ilk rc sayisi = 1
b sonraki oge = Some(RefCell { value: Dugum(5, RefCell { value: Bos }) })
a degistikten sonra b rc sayisi = 2
a degistikten sonra a rc sayisi = 2
ayı bye bağladıktan sonra hem a hem b için referans sayısı 2 olur.
main sonunda Rust önce byi bırakır; ama sayısı 1e düştüğü için belleği
silinmez. Sonra ayı bırakır; onun sayısı da 1e düşer. Böylece iki liste de
öbekte sonsuza kadar kalır.
Şekil 15-4: Birbirini işaret eden a ve b listelerinin oluşturduğu referans döngüsü
Son println!in yorumunu kaldırırsanız Rust bu döngüyü yazdırmaya çalışır;
adan bye, bden aya giderek sonsuza kadar ilerler ve sonunda yığın taşar.
Gerçek programlarda bunun sonucu daha ciddi olabilir. Büyük miktarda bellek ayıran döngüler uzun süre tutulursa program gereğinden fazla bellek tüketir ve sistemi zorlayabilir.
Referans döngüsü oluşturmak kolay değildir ama imkânsız da değildir. İçsel değiştirilebilirlik ve referans sayımı kullanan iç içe türlerle çalışırken döngü kurmadığınızdan siz emin olmalısınız; Rust bunu otomatik yakalayamaz. Bu tür hatalar mantık hatasıdır; otomatik test, kod incelemesi ve iyi geliştirme alışkanlıklarıyla azaltılmalıdır.
Bir başka çözüm de veri yapısını, bazı referanslar sahipliği ifade ederken bazıları etmeyecek şekilde yeniden düzenlemektir. Böylece döngü içinde sahiplik ifade etmeyen referanslar kullanabilir ve gerçek bırakma kararını yalnızca sahiplik ilişkileriyle sınırlayabilirsiniz.
Weak<T> Kullanarak Referans Döngülerini Önlemek
Rc::clone çağrısının strong_countu artırdığını ve bir Rc<T> örneğinin
yalnızca strong_count sıfıra düştüğünde temizlendiğini gördük. Aynı değere
zayıf referans oluşturmak için Rc::downgrade kullanabilirsiniz. Bu, Weak<T>
adlı akıllı işaretçiyi üretir.
Güçlü referanslar (Rc<T>), sahipliği paylaşır. Zayıf referanslar
(Weak<T>) ise sahiplik ilişkisi ifade etmez; sayıları, değerin ne zaman
temizleneceğini etkilemez. Bu yüzden zayıf referans içeren döngüler, güçlü
referans sayısı sıfıra indiğinde kırılmış olur.
Rc::downgrade çağrısı strong_countu değil weak_countu artırır. weak_count
değerin kaç Weak<T> tarafından izlendiğini tutar. Fark şudur: Rc<T>nin
temizlenmesi için weak_countun sıfır olması gerekmez.
Weak<T>nin gösterdiği değer zaten düşürülmüş olabilir. Bu yüzden Weak<T>yi
gerçekten kullanmadan önce hâlâ geçerli olup olmadığını kontrol etmelisiniz.
Bunun için upgrade çağrılır; sonuç Option<Rc<T>> olur. Değer yaşıyorsa
Some, düşürülmüşse None alırsınız.
Bunu görmek için, yalnızca sonraki öğeyi bilen liste yerine çocuklarını ve ebeveynlerini bilen ağaç düğümleri kuracağız.
Ağaç Veri Yapısı Oluşturmak
İlk olarak çocuk düğümlerini bilen bir ağaç oluşturalım. Kendi i32 değerini ve
çocuk düğümlere referansları tutan Node yapısını tanımlıyoruz:
Dosya Adı: src/main.rs
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
deger: i32,
cocuklar: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let yaprak = Rc::new(Node {
deger: 3,
cocuklar: RefCell::new(vec![]),
});
let dal = Rc::new(Node {
deger: 5,
cocuklar: RefCell::new(vec![Rc::clone(&yaprak)]),
});
}
Bir Node çocuklarının sahibi olsun, ama değişkenler de tek tek düğümlere
erişebilsin istiyoruz. Bunun için Vec<T> öğelerini Rc<Node> yapıyoruz.
Çocuk listesini değiştirmek isteyebileceğimiz için Vec<Rc<Node>>yi
RefCell<T> içine alıyoruz.
Sonra, çocuksuz ve değeri 3 olan yaprak düğümünü ve değeri 5 olan, çocuk
olarak yaprakı içeren dal düğümünü oluşturuyoruz.
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
deger: i32,
cocuklar: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let yaprak = Rc::new(Node {
deger: 3,
cocuklar: RefCell::new(vec![]),
});
let dal = Rc::new(Node {
deger: 5,
cocuklar: RefCell::new(vec![Rc::clone(&yaprak)]),
});
}
yaprak dugumu ve cocuk olarak yapraki iceren dal dugumu olusturmakyapraktaki Rc<Node>yi klonlayıp dal içine koyuyoruz; yani yaprak
artık iki sahipli. daldan yapraka dal.cocuklar yoluyla gidebiliriz; ama
yapraktan dala dönemeyiz. Çünkü yaprak, dalı tanımaz. Şimdi bunu
düzelteceğiz.
Çocuktan Ebeveyne Referans Eklemek
Çocuk düğümün ebeveynini bilebilmesi için Node tanımına ebeveyn alanı
eklemeliyiz. Ama bunun türü Rc<T> olamaz; aksi halde yaprak.ebeveyn,
dalı; dal.cocuklar da yaprakı güçlü biçimde tutar ve döngü oluşur.
İlişkiye başka açıdan bakınca çözüm netleşir: ebeveyn çocuğunun sahibi olmalı, ama çocuk ebeveyninin sahibi olmamalıdır. İşte bu zayıf referans senaryosudur.
Bu nedenle ebeveyn alanını Rc<T> değil, Weak<T> yapacağız; daha doğrusu
RefCell<Weak<Node>>. Liste 15-28 bunu gösterir.
Dosya Adı: src/main.rs
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
deger: i32,
ebeveyn: RefCell<Weak<Node>>,
cocuklar: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let yaprak = Rc::new(Node {
deger: 3,
ebeveyn: RefCell::new(Weak::new()),
cocuklar: RefCell::new(vec![]),
});
println!("yaprak ebeveyni = {:?}", yaprak.ebeveyn.borrow().upgrade());
let dal = Rc::new(Node {
deger: 5,
ebeveyn: RefCell::new(Weak::new()),
cocuklar: RefCell::new(vec![Rc::clone(&yaprak)]),
});
*yaprak.ebeveyn.borrow_mut() = Rc::downgrade(&dal);
println!("yaprak ebeveyni = {:?}", yaprak.ebeveyn.borrow().upgrade());
}
Bir düğüm ebeveynini işaret edebilir ama ona sahip olmaz. Liste 15-28’de
yaprak, dalı ebeveyni olarak görecek şekilde maini güncelliyoruz.
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
deger: i32,
ebeveyn: RefCell<Weak<Node>>,
cocuklar: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let yaprak = Rc::new(Node {
deger: 3,
ebeveyn: RefCell::new(Weak::new()),
cocuklar: RefCell::new(vec![]),
});
println!("yaprak ebeveyni = {:?}", yaprak.ebeveyn.borrow().upgrade());
let dal = Rc::new(Node {
deger: 5,
ebeveyn: RefCell::new(Weak::new()),
cocuklar: RefCell::new(vec![Rc::clone(&yaprak)]),
});
*yaprak.ebeveyn.borrow_mut() = Rc::downgrade(&dal);
println!("yaprak ebeveyni = {:?}", yaprak.ebeveyn.borrow().upgrade());
}
yaprak dugumuyaprak oluşturulurken ebeveyn alanı için boş bir Weak<Node> koyuyoruz.
Bu yüzden ilk println! çağrısında upgrade sonucu None olur:
yaprak ebeveyni = None
dalı oluşturduktan sonra yaprak.ebeveyn alanına Rc::downgrade(&dal)
sonucunu yazarız. Böylece yaprak, ebeveynini bilebilir ama ona sahip olmaz.
İkinci yazdırmada Some(...) görürüz. Üstelik yazdırılan yapıda Weak
etiketleri göründüğü için döngü olmadığını da anlarız.
strong_count ve weak_count Değişimini Görselleştirmek
Şimdi strong_count ve weak_count değerlerinin nasıl değiştiğine bakalım.
Bunun için dal oluşturmayı iç kapsama alacağız; böylece kapsam bitince ne
olduğunu net görürüz. Liste 15-29 değişiklikleri gösterir.
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
deger: i32,
ebeveyn: RefCell<Weak<Node>>,
cocuklar: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let yaprak = Rc::new(Node {
deger: 3,
ebeveyn: RefCell::new(Weak::new()),
cocuklar: RefCell::new(vec![]),
});
println!(
"yaprak guclu = {}, zayif = {}",
Rc::strong_count(&yaprak),
Rc::weak_count(&yaprak),
);
{
let dal = Rc::new(Node {
deger: 5,
ebeveyn: RefCell::new(Weak::new()),
cocuklar: RefCell::new(vec![Rc::clone(&yaprak)]),
});
*yaprak.ebeveyn.borrow_mut() = Rc::downgrade(&dal);
println!(
"dal guclu = {}, zayif = {}",
Rc::strong_count(&dal),
Rc::weak_count(&dal),
);
println!(
"yaprak guclu = {}, zayif = {}",
Rc::strong_count(&yaprak),
Rc::weak_count(&yaprak),
);
}
println!("yaprak ebeveyni = {:?}", yaprak.ebeveyn.borrow().upgrade());
println!(
"yaprak guclu = {}, zayif = {}",
Rc::strong_count(&yaprak),
Rc::weak_count(&yaprak),
);
}
dali ic kapsamda olusturup guclu ve zayif referans sayilarini incelemekyaprak ilk oluşturulduğunda güçlü sayı 1, zayıf sayı 0dır. İç kapsamda
dal oluşturulup yaprakla ilişkilendirilince, dalın güçlü sayısı 1, zayıf
sayısı 1 olur; çünkü yaprak.ebeveyn, dala zayıf referans verir. Bu sırada
yaprakın güçlü sayısı 2 olur; çünkü dal.cocuklar, yaprakı da güçlü
şekilde tutar.
İç kapsam bittiğinde dal kapsam dışına çıkar; güçlü sayı 0 olduğu için
Node düşürülür. yaprak.ebeveyndeki zayıf referans buna engel olmaz; bu
yüzden bellek sızıntısı oluşmaz.
Kapsamdan sonra yaprakın ebeveynine yeniden erişmeyi denersek yine None
alırız. Program sonunda yaprak yalnız kaldığı için güçlü sayı 1, zayıf sayı
0dır.
Sayaçları yönetme ve değerleri bırakma mantığının tamamı Rc<T> ve Weak<T>
içinde, Drop uygulamalarıyla birlikte gelir. Çocuktan ebeveyne giden ilişkinin
Weak<T> olacağını Node tanımında belirleyerek, ebeveyn ve çocukların
birbirini gösterebildiği ama referans döngüsü üretmeyen bir yapı kurabilirsiniz.
Özet
Bu bölüm, akıllı işaretçileri kullanarak Rust’ın normal referanslarla varsayılan
olarak sunduğundan farklı güvenceler ve takaslar elde etmeyi anlattı. Box<T>
bilinen boyuta sahip olup öbekteki veriyi işaret eder. Rc<T>, aynı verinin
birden çok sahibi olabilmesi için referans sayısını tutar. RefCell<T> ise
değiştirilemez bir dış türü korurken içteki değeri değiştirmemize izin verir ve
ödünç alma kurallarını derleme zamanında değil çalışma zamanında uygular.
Ayrıca akıllı işaretçilerin sunduğu pek çok davranışı mümkün kılan Deref ve
Drop trait’lerini de gördük. Referans döngülerinin bellek sızıntısına nasıl
yol açabileceğini ve Weak<T> kullanarak nasıl önlenebileceğini de inceledik.
Bu bölüm ilginizi çektiyse ve kendi akıllı işaretçilerinizi yazmak istiyorsanız, “The Rustonomicon” size daha fazla ayrıntı sunar.
Sırada Rust’ta eşzamanlılık var. Orada da birkaç yeni akıllı işaretçi göreceğiz.