পাঠ ১৫.৬

Reference Cycle থেকে memory leak

Reference Cycles Can Leak Memory

Rust-এর memory safety guarantee accidental memory leak কঠিন করে দেয় — কিন্তু impossible না। Rust সব memory error prevent করে না; memory leak Rust-এ memory-safe বলে গণ্য। Rc<T> এবং RefCell<T> একসাথে use করলে reference cycle বানানো সম্ভব — যেখানে item-গুলো একে অপরকে point করে। তখন reference count কখনো 0 হয় না, value drop হয় না — leak।

Cycle তৈরি করা

আগের cons list-এর একটা variant দেখব — দ্বিতীয় element-এ RefCell<Rc<List>>। মানে — Cons variant-এর list-pointer-টা mutate করা যাবে (i32 না)।

src/main.rsrust
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() {}

tail method — Cons হলে দ্বিতীয় element-এর reference, Nil হলে None।

এবার cycle:

src/main.rsrust
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());
}

কী ঘটল:

  1. a = Cons(5, → Nil)।
  2. b = Cons(10, → a)।
  3. তারপর a-এর tail-কে modify করে b-তে point করানো — এখন a → b → a।

Output:

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

Modification-এর পর — দু'টোরই count 2। main শেষে — Rust b drop করতে চাইবে (b-এর local count কমে 1, কিন্তু a-এর tail এখনো b-কে hold করছে); a drop করতে চাইবে (a-এর local count কমে 1, কিন্তু b a-কে hold)। দু'জনেরই count ১-ই থেকে যায়। কেউ drop হয় না — leak।

শেষের commented line uncomment করলে — list traverse infinite, stack overflow।

Production-এ এটা bug। সমাধান — data structure-এর design-এ ownership relationship আর reference relationship আলাদা রাখা।

Weak<T> — non-owning reference

Rc::clone strong reference বাড়ায় — মানে ownership। Rc::downgrade Weak<T> বানায় — non-owning, weak count বাড়ে; strong count না।

Rc drop হবে যখন strong count 0; weak count যাই হোক না কেন। মানে — cycle-এ weak reference থাকলে cycle break, leak হয় না।

কিন্তু weak reference থেকে actual value access করতে গেলে — value এখনো আছে কিনা সেটা confirm করতে হবে। তাই Weak<T>-এ upgrade method — Option<Rc<T>> return; value থাকলে Some, drop হয়ে গেলে None

Tree data structure

একটা Node — যার একটা value আর একগুচ্ছ children।

src/main.rsrust
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)]),
    });
}

branch তার children-এ leaf-কে own করছে — Rc clone। কিন্তু এখন leaf থেকে branch (parent) access করার উপায় নেই। Parent reference যোগ করতে চাইলে — সেটা Rc করলে cycle।

Parent reference Weak

src/main.rsrust
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());
}

Design choice — parent → child Rc (ownership), child → parent Weak (non-owning)। Result:

  • Parent drop হলে children drop।
  • Child drop-এ parent affect না।
  • কোনো cycle নেই — leak নেই।

প্রথম println!-এ leaf-এর parent এখনো Weak::new() (default empty) — upgrade None। পরে branch বানিয়ে *leaf.parent.borrow_mut() = Rc::downgrade(&branch); — leaf-এর parent এখন branch-কে weak-point করে। দ্বিতীয় println-এ upgrade Some(Node)।

Strong/weak count visualize

src/main.rsrust
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),
    );
}

Behavior:

  • Initial leaf — strong 1, weak 0।
  • branch বানানোর পর — leaf strong 2 (নিজে + branch.children), weak 0।
  • downgrade-এর পর — branch strong 1, weak 1 (leaf.parent থেকে)।
  • inner scope শেষ হলে — branch-এর strong 0, drop। weak 1 ছিল কিন্তু সেটা drop আটকায় না।
  • শেষে — leaf strong 1, weak 0; leaf.parent.upgrade() এখন None (branch গেছে)।

এই design pattern — parent owns child via Rc, child references parent via Weak — tree, graph, DOM-এর মতো structure-এর standard solution।

এই পাঠ থেকে যা শিখলে

  • Reference cycle = Rc + RefCell দিয়ে item-গুলো একে অপরকে point করলে — count 0 হয় না, leak।
  • Memory leak Rust-এ memory-safe; কিন্তু design bug।
  • Weak<T> non-owning; Rc::downgrade দিয়ে; weak count বাড়ে, strong না।
  • upgrade Option<Rc<T>> দেয় — value existence check।
  • Tree pattern — parent → child Rc, child → parent Weak; cycle-free।