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

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.

Filename: src/main.rs
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() {}
Listing 15-25: Dugum varyantinin gosterdigi seyi degistirebilmek icin RefCell<T> tutan kons liste tanimi

Bu, 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.

Filename: src/main.rs
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());
}
Listing 15-26: Birbirini gosteren iki Liste degeriyle referans dongusu olusturmak

a 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 aBos 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

abye 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.

'a' 5 degerini, 'b' 10 degerini gosteren iki kutu vardir; 5'i tutan kutu 10'a, 10'u tutan kutu tekrar 5'e donerek dongu olusturur.

Ş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.

Filename: 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)]),
    });
}
Listing 15-27: Cocuksuz yaprak dugumu ve cocuk olarak yapraki iceren dal dugumu olusturmak

yapraktaki 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.

Filename: 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());
}
Listing 15-28: Ebeveynine zayif referans veren yaprak dugumu

yaprak 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.

Filename: 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 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),
    );
}
Listing 15-29: dali ic kapsamda olusturup guclu ve zayif referans sayilarini incelemek

yaprak 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.