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

دورات المراجع يمكن أن تسرب الذاكرة (Reference Cycles Can Leak Memory)

تجعل ضمانات سلامة الذاكرة في Rust من الصعب، ولكن ليس من المستحيل، إنشاء ذاكرة لا يتم تنظيفها أبداً عن طريق الخطأ (ما يعرف باسم تسرب الذاكرة (memory leak)). إن منع تسرب الذاكرة تماماً ليس أحد ضمانات Rust، مما يعني أن تسرب الذاكرة يعتبر آمناً من حيث الذاكرة (memory safe) في Rust. يمكننا أن نرى أن Rust يسمح بتسرب الذاكرة باستخدام Rc<T> و RefCell<T>: من الممكن إنشاء مراجع حيث تشير العناصر إلى بعضها البعض في دورة (cycle). يؤدي هذا إلى حدوث memory leaks لأن عدد المراجع (reference count) لكل عنصر في الدورة لن يصل أبداً إلى 0، ولن يتم إسقاط (drop) القيم أبداً.

إنشاء دورة مراجع (Creating a Reference Cycle)

دعونا نلقي نظرة على كيفية حدوث دورة مراجع وكيفية منعها، بدءاً من تعريف التعداد List ودالة tail في القائمة 15-25.

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() {}

نحن نستخدم تنويعاً آخر لتعريف List من القائمة 15-5. العنصر الثاني في متغير Cons هو الآن RefCell<Rc<List>> ، مما يعني أنه بدلاً من امتلاك القدرة على تعديل قيمة i32 كما فعلنا في القائمة 15-24، نريد تعديل قيمة List التي يشير إليها متغير Cons. نضيف أيضاً دالة tail لتسهيل الوصول إلى العنصر الثاني إذا كان لدينا متغير Cons.

في القائمة 15-26، نضيف دالة main تستخدم التعريفات الموجودة في القائمة 15-25. ينشئ هذا الكود قائمة في a وقائمة في b تشير إلى القائمة في a. ثم يقوم بتعديل القائمة في a لتشير إلى b ، مما يؤدي إلى إنشاء دورة مراجع (reference cycle). توجد عبارات println! على طول الطريق لتوضيح عدد المراجع في نقاط مختلفة من هذه العملية.

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());
}

ننشئ مثيلاً (instance) من Rc<List> يحمل قيمة List في المتغير a مع قائمة أولية هي 5, Nil. ثم ننشئ instance من Rc<List> يحمل قيمة List أخرى في المتغير b تحتوي على القيمة 10 وتشير إلى القائمة في a.

نقوم بتعديل a بحيث تشير إلى b بدلاً من Nil ، مما يؤدي إلى إنشاء cycle. نفعل ذلك باستخدام دالة tail للحصول على مرجع لـ RefCell<Rc<List>> في a ، والذي نضعه في المتغير link. ثم نستخدم دالة borrow_mut على RefCell<Rc<List>> لتغيير القيمة بالداخل من Rc<List> يحمل قيمة Nil إلى Rc<List> الموجود في b.

عندما نشغل هذا الكود، مع ترك آخر println! كتعليق في الوقت الحالي، سنحصل على هذه المخرجات:

$ 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

يصبح reference count لمثيلات Rc<List> في كل من a و b هو 2 بعد تغيير القائمة في a لتشير إلى b. في نهاية main ، يقوم Rust بإسقاط المتغير b ، مما يقلل reference count لمثيل b من نوع Rc<List> من 2 إلى 1. لن يتم إسقاط الذاكرة التي يمتلكها Rc<List> في الكومة (heap) في هذه المرحلة لأن reference count الخاص به هو 1 وليس 0. ثم يقوم Rust بإسقاط a ، مما يقلل reference count لمثيل a من نوع Rc<List> من 2 إلى 1 أيضاً. لا يمكن إسقاط ذاكرة هذا المثيل أيضاً، لأن مثيل Rc<List> الآخر لا يزال يشير إليه. ستبقى الذاكرة المخصصة للقائمة غير مجمعة للأبد. لتصور دورة المراجع هذه، أنشأنا الرسم التخطيطي في الشكل 15-4.

مستطيل مسمى 'a' يشير إلى مستطيل يحتوي على العدد الصحيح 5. مستطيل مسمى 'b' يشير إلى مستطيل يحتوي على العدد الصحيح 10. المستطيل الذي يحتوي على 5 يشير إلى المستطيل الذي يحتوي على 10، والمستطيل الذي يحتوي على 10 يشير مرة أخرى إلى المستطيل الذي يحتوي على 5، مما يؤدي إلى إنشاء دورة.

الشكل 15-4: دورة مراجع للقائمتين a و b تشيران إلى بعضهما البعض

إذا قمت بإلغاء التعليق عن آخر println! وشغلت البرنامج، فسيحاول Rust طباعة هذه الدورة مع إشارة a إلى b وإشارة b إلى a وهكذا دواليك حتى يحدث فيض في المكدس (stack overflow).

بالمقارنة مع برنامج حقيقي، فإن عواقب إنشاء reference cycle في هذا المثال ليست وخيمة للغاية: فمباشرة بعد إنشاء دورة المراجع، ينتهي البرنامج. ومع ذلك، إذا قام برنامج أكثر تعقيداً بتخصيص الكثير من الذاكرة في دورة واحتفظ بها لفترة طويلة، فسيستخدم البرنامج ذاكرة أكثر مما يحتاج وقد يرهق النظام، مما يتسبب في نفاد الذاكرة المتاحة.

إن إنشاء دورات المراجع ليس بالأمر السهل، ولكنه ليس مستحيلاً أيضاً. إذا كان لديك قيم RefCell<T> تحتوي على قيم Rc<T> أو تركيبات متداخلة مماثلة من الأنواع ذات القابلية للتغيير الداخلية (interior mutability) وعد المراجع (reference counting)، فيجب عليك التأكد من عدم إنشاء دورات؛ لا يمكنك الاعتماد على Rust لاكتشافها. سيكون إنشاء reference cycle بمثابة خطأ منطقي (logic bug) في برنامجك يجب عليك استخدام الاختبارات المؤتمتة ومراجعات الكود وممارسات تطوير البرمجيات الأخرى لتقليله.

حل آخر لتجنب دورات المراجع هو إعادة تنظيم هياكل البيانات الخاصة بك بحيث تعبر بعض المراجع عن الملكية (ownership) وبعض المراجع لا تفعل ذلك. ونتيجة لذلك، يمكن أن يكون لديك دورات تتكون من بعض علاقات الملكية وبعض العلاقات غير المملوكة، وفقط علاقات الملكية هي التي تؤثر على ما إذا كان يمكن إسقاط القيمة أم لا. في القائمة 15-25، نريد دائماً أن تمتلك متغيرات Cons قائمتها، لذا فإن إعادة تنظيم هيكل البيانات غير ممكنة. دعونا نلقي نظرة على مثال يستخدم الرسوم البيانية (graphs) المكونة من عقد أب (parent nodes) وعقد أبناء (child nodes) لنرى متى تكون العلاقات غير المملوكة وسيلة مناسبة لمنع دورات المراجع.

منع دورات المراجع باستخدام Weak<T> (Preventing Reference Cycles Using Weak)

حتى الآن، أوضحنا أن استدعاء Rc::clone يزيد من العدد القوي (strong_count) لمثيل Rc<T> ، ولا يتم تنظيف مثيل Rc<T> إلا إذا كان strong_count الخاص به هو 0. يمكنك أيضاً إنشاء مرجع ضعيف (weak reference) للقيمة داخل مثيل Rc<T> عن طريق استدعاء Rc::downgrade وتمرير مرجع إلى Rc<T>. المراجع القوية (Strong references) هي الطريقة التي يمكنك من خلالها مشاركة ملكية مثيل Rc<T>. المراجع الضعيفة (Weak references) لا تعبر عن علاقة ملكية، ولا يؤثر عددها على وقت تنظيف مثيل Rc<T>. لن تتسبب في حدوث reference cycle، لأن أي دورة تتضمن بعض المراجع الضعيفة سيتم كسرها بمجرد أن يصبح strong_count للقيم المعنية 0.

عندما تستدعي Rc::downgrade ، تحصل على مؤشر ذكي (smart pointer) من نوع Weak<T>. بدلاً من زيادة strong_count في مثيل Rc<T> بمقدار 1، فإن استدعاء Rc::downgrade يزيد العدد الضعيف (weak_count) بمقدار 1. يستخدم النوع Rc<T> الـ weak_count لتتبع عدد مراجع Weak<T> الموجودة، بشكل مشابه لـ strong_count. الفرق هو أن weak_count لا يحتاج إلى أن يكون 0 حتى يتم تنظيف مثيل Rc<T>.

نظراً لأن القيمة التي يشير إليها Weak<T> قد تكون قد أُسقطت، فلكي تفعل أي شيء بالقيمة التي يشير إليها Weak<T> ، يجب عليك التأكد من أن القيمة لا تزال موجودة. افعل ذلك عن طريق استدعاء دالة upgrade على مثيل Weak<T> ، والتي ستعيد Option<Rc<T>>. ستحصل على نتيجة Some إذا لم تكن قيمة Rc<T> قد أُسقطت بعد، ونتيجة None إذا كانت قيمة Rc<T> قد أُسقطت. نظراً لأن upgrade تعيد Option<Rc<T>> ، فإن Rust سيضمن التعامل مع حالة Some وحالة None ، ولن يكون هناك مؤشر غير صالح (invalid pointer).

كمثال، بدلاً من استخدام قائمة تعرف عناصرها فقط عن العنصر التالي، سننشئ شجرة (tree) تعرف عناصرها عن عناصرها الأبناء وعناصرها الآباء أيضاً.

إنشاء هيكل بيانات الشجرة (Creating a Tree Data Structure)

للبدء، سنبني شجرة تحتوي على عقد (nodes) تعرف عن عقدها الأبناء. سننشئ struct يسمى Node يحمل قيمة i32 الخاصة به بالإضافة إلى مراجع لقيم Node الأبناء الخاصة به:

اسم الملف: 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)]),
    });
}

نريد أن تمتلك Node أبناءها، ونريد مشاركة تلك الملكية مع المتغيرات حتى نتمكن من الوصول إلى كل Node في الشجرة مباشرة. للقيام بذلك، نعرف عناصر Vec<T> لتكون قيماً من نوع Rc<Node>. نريد أيضاً تعديل العقد التي تعتبر أبناء لعقدة أخرى، لذا لدينا RefCell<T> في children حول Vec<Rc<Node>>.

بعد ذلك، سنستخدم تعريف struct الخاص بنا وننشئ مثيل Node واحداً يسمى leaf بالقيمة 3 وبدون أبناء، ومثيلاً آخر يسمى branch بالقيمة 5 و leaf كأحد أبنائه، كما هو موضح في القائمة 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)]),
    });
}

نقوم بعمل clone لـ Rc<Node> في leaf ونخزن ذلك في branch ، مما يعني أن Node في leaf أصبح لها الآن مالكان: leaf و branch. يمكننا الانتقال من branch إلى leaf عبر branch.children ، ولكن لا توجد طريقة للانتقال من leaf إلى branch. والسبب هو أن leaf ليس لديها مرجع لـ branch ولا تعرف أنهما مرتبطان. نريد أن تعرف leaf أن branch هو والدها. سنفعل ذلك بعد ذلك.

إضافة مرجع من الابن إلى والده

لجعل العقدة الابنة مدركة لوالدها، نحتاج إلى إضافة حقل parent إلى تعريف struct الخاص بـ Node. تكمن المشكلة في تحديد نوع parent. نحن نعلم أنه لا يمكن أن يحتوي على Rc<T> ، لأن ذلك سيؤدي إلى إنشاء دورة مراجع مع إشارة leaf.parent إلى branch وإشارة branch.children إلى leaf ، مما قد يتسبب في ألا تصبح قيم strong_count الخاصة بهما 0 أبداً.

بالتفكير في العلاقات بطريقة أخرى، يجب أن تمتلك العقدة الأب أبناءها: إذا تم إسقاط عقدة أب، فيجب إسقاط عقدها الأبناء أيضاً. ومع ذلك، لا ينبغي للابن أن يمتلك والده: إذا أسقطنا عقدة ابنة، فيجب أن يظل الأب موجوداً. هذه حالة للمراجع الضعيفة!

لذا، بدلاً من Rc<T> ، سنجعل نوع parent يستخدم Weak<T> ، وتحديداً RefCell<Weak<Node>>. الآن يبدو تعريف struct الخاص بـ Node كما يلي:

اسم الملف: 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());
}

ستتمكن العقدة من الرجوع إلى عقدتها الأب ولكنها لا تمتلك والدها. في القائمة 15-28، نقوم بتحديث main لاستخدام هذا التعريف الجديد بحيث يكون لدى عقدة leaf طريقة للرجوع إلى والدها، 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());
}

يبدو إنشاء عقدة leaf مشابهاً للقائمة 15-27 باستثناء حقل parent: تبدأ leaf بدون أب، لذا ننشئ مثيل مرجع Weak<Node> جديداً وفارغاً.

في هذه المرحلة، عندما نحاول الحصول على مرجع لوالد leaf باستخدام دالة upgrade ، نحصل على قيمة None. نرى هذا في المخرجات من أول عبارة println!:

leaf parent = None

عندما ننشئ عقدة branch ، سيكون لها أيضاً مرجع Weak<Node> جديد في حقل parent لأن branch ليس لها عقدة أب. لا نزال نملك leaf كأحد أبناء branch. بمجرد حصولنا على مثيل Node في branch ، يمكننا تعديل leaf لمنحها مرجع Weak<Node> لوالدها. نستخدم دالة borrow_mut على RefCell<Weak<Node>> في leaf ، ونستخدم دالة Rc::downgrade من Rc<Node> في branch لإنشاء مرجع Weak<Node> لـ branch.

عندما نطبع والد leaf مرة أخرى، سنحصل هذه المرة على متغير Some يحمل branch: الآن يمكن لـ leaf الوصول إلى والدها! عندما نطبع leaf ، نتجنب أيضاً الدورة التي انتهت في النهاية بفيض في المكدس كما حدث في القائمة 15-26؛ حيث تتم طباعة مراجع Weak<Node> كـ (Weak):

leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })

يشير عدم وجود مخرجات لانهائية إلى أن هذا الكود لم ينشئ دورة مراجع. يمكننا أيضاً معرفة ذلك من خلال النظر في القيم التي نحصل عليها من استدعاء Rc::strong_count و Rc::weak_count.

تصور التغييرات في strong_count و weak_count

دعونا نلقي نظرة على كيفية تغير قيم strong_count و weak_count لمثيلات Rc<Node> من خلال إنشاء نطاق داخلي جديد ونقل عملية إنشاء branch إلى ذلك النطاق. من خلال القيام بذلك، يمكننا رؤية ما يحدث عند إنشاء branch ثم إسقاطها عندما تخرج عن النطاق (scope). تظهر التعديلات في القائمة 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),
    );
}

بعد إنشاء leaf ، يكون لـ Rc<Node> الخاص بها عدد قوي (strong count) قدره 1 وعدد ضعيف (weak count) قدره 0. في النطاق الداخلي، ننشئ branch ونربطها بـ leaf ، وعند هذه النقطة عندما نطبع الأعداد، سيكون لـ Rc<Node> في branch عدد قوي قدره 1 وعدد ضعيف قدره 1 (لأن leaf.parent تشير إلى branch باستخدام Weak<Node>). عندما نطبع الأعداد في leaf ، سنرى أن لها عدداً قوياً قدره 2 لأن branch لديها الآن نسخة (clone) من Rc<Node> الخاص بـ leaf مخزنة في branch.children ولكن سيظل لها عدد ضعيف قدره 0.

عندما ينتهي النطاق الداخلي، تخرج branch عن النطاق وينخفض العدد القوي لـ Rc<Node> إلى 0، لذا يتم إسقاط Node الخاصة بها. العدد الضعيف 1 من leaf.parent ليس له أي تأثير على ما إذا كان سيتم إسقاط Node أم لا، لذا لا نحصل على أي تسرب للذاكرة!

إذا حاولنا الوصول إلى والد leaf بعد نهاية النطاق، فسنحصل على None مرة أخرى. في نهاية البرنامج، يكون لـ Rc<Node> في leaf عدد قوي قدره 1 وعدد ضعيف قدره 0 لأن المتغير leaf هو الآن المرجع الوحيد لـ Rc<Node> مرة أخرى.

كل المنطق الذي يدير الأعداد وإسقاط القيم مدمج في Rc<T> و Weak<T> وتنفيذاتهما لسمة Drop. من خلال تحديد أن العلاقة من الابن إلى والده يجب أن تكون مرجع Weak<T> في تعريف Node ، يمكنك جعل عقد الآباء تشير إلى عقد الأبناء والعكس صحيح دون إنشاء دورة مراجع وتسرب للذاكرة.

ملخص (Summary)

غطى هذا الفصل كيفية استخدام المؤشرات الذكية (smart pointers) لتقديم ضمانات ومقايضات مختلفة عن تلك التي يقدمها Rust افتراضياً مع المراجع العادية. النوع Box<T> له حجم معروف ويشير إلى بيانات مخصصة في الكومة (heap). النوع Rc<T> يتتبع عدد المراجع للبيانات في الكومة بحيث يمكن أن يكون للبيانات مالكون متعددون. النوع RefCell<T> مع قابليته للتغيير الداخلية يمنحنا نوعاً يمكننا استخدامه عندما نحتاج إلى نوع غير قابل للتغيير ولكننا نحتاج إلى تغيير قيمة داخلية لذلك النوع؛ كما أنه يفرض قواعد الاستعارة (borrowing rules) في وقت التشغيل بدلاً من وقت التصريف.

تمت أيضاً مناقشة سمات Deref و Drop ، والتي تتيح الكثير من وظائف المؤشرات الذكية. استكشفنا دورات المراجع التي يمكن أن تسبب تسرباً للذاكرة وكيفية منعها باستخدام Weak<T>.

إذا كان هذا الفصل قد أثار اهتمامك وتريد تنفيذ مؤشراتك الذكية الخاصة، فراجع كتاب “The Rustonomicon” لمزيد من المعلومات المفيدة.

بعد ذلك، سنتحدث عن التزامن (concurrency) في Rust. ستتعلم حتى عن بعض المؤشرات الذكية الجديدة.