Referenzzyklen können Speicher verlieren

RustRustBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu Reference Cycles Can Leak Memory. Dieser Lab ist Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab untersuchen wir, wie Rusts Garantien für die Arbeitsspeicher-Sicherheit es schwierig, aber nicht unmöglich macht, versehentlich Arbeitsspeicher-Lecks zu erzeugen, insbesondere wenn Rc<T> und RefCell<T> verwendet werden, was zu Referenzzirkeln führen kann, die verhindern, dass Werte gelöscht werden und somit Arbeitsspeicher verlieren.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100436{{"Referenzzyklen können Speicher verlieren"}} rust/integer_types -.-> lab-100436{{"Referenzzyklen können Speicher verlieren"}} rust/function_syntax -.-> lab-100436{{"Referenzzyklen können Speicher verlieren"}} rust/expressions_statements -.-> lab-100436{{"Referenzzyklen können Speicher verlieren"}} rust/method_syntax -.-> lab-100436{{"Referenzzyklen können Speicher verlieren"}} rust/traits -.-> lab-100436{{"Referenzzyklen können Speicher verlieren"}} rust/operator_overloading -.-> lab-100436{{"Referenzzyklen können Speicher verlieren"}} end

Referenzzirkel können Arbeitsspeicher verlieren

Rusts Garantien für die Arbeitsspeicher-Sicherheit machen es schwierig, aber nicht unmöglich, versehentlich Arbeitsspeicher zu erzeugen, der nie bereinigt wird (ein sogenannter Arbeitsspeicher-Leck). Die vollständige Verhinderung von Arbeitsspeicher-Lecks ist keine der Garantien von Rust, was bedeutet, dass Arbeitsspeicher-Lecks in Rust arbeitspeichersicher sind. Wir können sehen, dass Rust Arbeitsspeicher-Lecks zulässt, indem Rc<T> und RefCell<T> verwendet werden: Es ist möglich, Referenzen zu erstellen, bei denen Elemente sich in einem Zyklus aufeinander beziehen. Dies verursacht Arbeitsspeicher-Lecks, da die Referenzzählung jedes Elements im Zyklus niemals 0 erreichen wird und die Werte niemals gelöscht werden.

Ein Referenzzyklus erstellen

Schauen wir uns an, wie ein Referenzzyklus auftreten kann und wie man ihn vermeiden kann. Wir beginnen mit der Definition der List-Enumeration und einer tail-Methode in Listing 15-25.

Dateiname: src/main.rs

use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
enum List {
  1 Cons(i32, RefCell<Rc<List>>),
    Nil,
}

impl List {
  2 fn tail(&self) -> Option<&RefCell<Rc<List>>> {
        match self {
            Cons(_, item) => Some(item),
            Nil => None,
        }
    }
}

Listing 15-25: Eine Definition einer Cons-Liste, die eine RefCell<T> enthält, um das zu verändern, auf das eine Cons-Variante zeigt

Wir verwenden eine andere Variation der List-Definition aus Listing 15-5. Das zweite Element in der Cons-Variante ist jetzt RefCell<Rc<List>> [1], was bedeutet, dass wir statt der Möglichkeit, den i32-Wert zu verändern, wie wir es in Listing 15-24 getan haben, den List-Wert verändern möchten, auf den eine Cons-Variante zeigt. Wir fügen auch eine tail-Methode hinzu [2], um es uns bequem zu machen, auf das zweite Element zuzugreifen, wenn wir eine Cons-Variante haben.

In Listing 15-26 fügen wir eine main-Funktion hinzu, die die Definitionen in Listing 15-25 verwendet. Dieser Code erstellt eine Liste in a und eine Liste in b, die auf die Liste in a zeigt. Anschließend ändert er die Liste in a, sodass sie auf b zeigt, was einen Referenzzyklus erzeugt. Es gibt println!-Anweisungen dazwischen, um anzuzeigen, was die Referenzzählungen zu verschiedenen Punkten in diesem Prozess sind.

Dateiname: src/main.rs

fn main() {
  1 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());

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

  3 if let Some(link) = a.tail() {
      4 *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)
    );

    // Entkommentieren Sie die nächste Zeile, um zu sehen, dass wir einen Zyklus haben;
    // es wird den Stack überlaufen
    // println!("a next item = {:?}", a.tail());
}

Listing 15-26: Erstellen eines Referenzzyklus von zwei List-Werten, die sich aufeinander verweisen

Wir erstellen eine Rc<List>-Instanz, die einen List-Wert in der Variable a enthält, mit einer initialen Liste von 5, Nil [1]. Anschließend erstellen wir eine Rc<List>-Instanz, die einen anderen List-Wert in der Variable b enthält, der den Wert 10 enthält und auf die Liste in a zeigt [2].

Wir ändern a, sodass es auf b statt auf Nil zeigt, was einen Zyklus erzeugt. Wir tun das, indem wir die tail-Methode verwenden, um auf die RefCell<Rc<List>> in a zu verweisen, die wir in die Variable link legen [3]. Anschließend verwenden wir die borrow_mut-Methode auf der RefCell<Rc<List>>, um den Wert darin von einer Rc<List>, die einen Nil-Wert enthält, zu einer Rc<List> in b zu ändern [4].

Wenn wir diesen Code ausführen und die letzte println!-Anweisung momentan kommentiert lassen, erhalten wir diese Ausgabe:

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

Die Referenzzählung der Rc<List>-Instanzen in sowohl a als auch b ist 2, nachdem wir die Liste in a geändert haben, sodass sie auf b zeigt. Am Ende von main verliert Rust die Variable b, was die Referenzzählung der b Rc<List>-Instanz von 2 auf 1 verringert. Der Arbeitsspeicher, den Rc<List> auf dem Heap hat, wird zu diesem Zeitpunkt nicht gelöscht, da seine Referenzzählung 1 und nicht 0 ist. Dann verliert Rust a, was auch die Referenzzählung der a Rc<List>-Instanz von 2 auf 1 verringert. Der Arbeitsspeicher dieser Instanz kann ebenfalls nicht gelöscht werden, da die andere Rc<List>-Instanz immer noch auf sie verweist. Der für die Liste zugewiesene Arbeitsspeicher bleibt für immer ungesammelt. Um diesen Referenzzyklus zu visualisieren, haben wir ein Diagramm in Abbildung 15-4 erstellt.

Abbildung 15-4: Ein Referenzzyklus von Listen a und b, die sich aufeinander verweisen

Wenn Sie die letzte println!-Anweisung entkommentieren und das Programm ausführen, wird Rust versuchen, diesen Zyklus mit a auf b auf a usw. anzuzeigen, bis der Stack überläuft.

Im Vergleich zu einem realen Programm sind die Folgen des Erstellens eines Referenzzyklus in diesem Beispiel nicht sehr schlimm: Kurz nachdem wir den Referenzzyklus erstellt haben, endet das Programm. Wenn jedoch ein komplexeres Programm in einem Zyklus viel Arbeitsspeicher allokiert und ihn lange Zeit hält, würde das Programm mehr Arbeitsspeicher verwenden, als es benötigt, und könnte das System überlasten, was dazu führen würde, dass es den verfügbaren Arbeitsspeicher erschöpft.

Das Erstellen von Referenzzyklen ist nicht leicht, aber es ist auch nicht unmöglich. Wenn Sie RefCell<T>-Werte haben, die Rc<T>-Werte enthalten oder ähnliche geschachtelte Kombinationen von Typen mit innerer Veränderbarkeit und Referenzzählung, müssen Sie sicherstellen, dass Sie keine Zyklen erzeugen; Sie können nicht darauf verlassen, dass Rust sie fängt. Das Erstellen eines Referenzzyklus wäre ein logischer Fehler in Ihrem Programm, den Sie mit automatisierten Tests, Code Reviews und anderen Softwareentwicklungspraktiken minimieren sollten.

Eine andere Möglichkeit, um Referenzzyklen zu vermeiden, besteht darin, Ihre Datenstrukturen umzuorganisieren, sodass einige Referenzen die Eigentumsbeziehung ausdrücken und einige Referenzen dies nicht tun. Dadurch können Sie Zyklen aus einigen Eigentumsbeziehungen und einigen Nicht-Eigentumsbeziehungen bilden, und nur die Eigentumsbeziehungen beeinflussen, ob ein Wert gelöscht werden kann. In Listing 15-25 möchten wir immer, dass Cons-Varianten ihre Liste besitzen, sodass eine Umorganisation der Datenstruktur nicht möglich ist. Schauen wir uns ein Beispiel mit Graphen aus Elternknoten und Kindknoten an, um zu sehen, wann Nicht-Eigentumsbeziehungen eine geeignete Möglichkeit sind, um Referenzzyklen zu vermeiden.

Vermeiden von Referenzzyklen mit Weak<T>{=html}

Bisher haben wir gezeigt, dass das Aufrufen von Rc::clone die strong_count einer Rc<T>-Instanz erhöht und dass eine Rc<T>-Instanz nur dann bereinigt wird, wenn ihre strong_count 0 ist. Sie können auch eine schwache Referenz auf den Wert innerhalb einer Rc<T>-Instanz erstellen, indem Sie Rc::downgrade aufrufen und eine Referenz auf die Rc<T> übergeben. Starke Referenzen sind die Art, wie Sie die Eigentumsverteilung einer Rc<T>-Instanz teilen können. Schwache Referenzen drücken keine Eigentumsbeziehung aus, und ihre Anzahl hat keinen Einfluss darauf, wann eine Rc<T>-Instanz bereinigt wird. Sie verursachen keinen Referenzzyklus, da jeder Zyklus, der einige schwache Referenzen umfasst, abgebrochen wird, sobald die starke Referenzzählung der beteiligten Werte 0 ist.

Wenn Sie Rc::downgrade aufrufen, erhalten Sie einen Smart-Pointer vom Typ Weak<T>. Anstatt die strong_count in der Rc<T>-Instanz um 1 zu erhöhen, erhöht das Aufrufen von Rc::downgrade die weak_count um 1. Der Rc<T>-Typ verwendet die weak_count, um zu verfolgen, wie viele Weak<T>-Referenzen existieren, ähnlich wie die strong_count. Der Unterschied ist, dass die weak_count nicht 0 sein muss, damit die Rc<T>-Instanz bereinigt wird.

Da der Wert, auf den Weak<T> verweist, möglicherweise bereits gelöscht wurde, müssen Sie sicherstellen, dass der Wert noch existiert, um etwas mit dem Wert zu tun, auf den eine Weak<T> zeigt. Dies tun Sie, indem Sie die upgrade-Methode auf einer Weak<T>-Instanz aufrufen, die ein Option<Rc<T>> zurückgibt. Sie erhalten ein Ergebnis von Some, wenn der Rc<T>-Wert noch nicht gelöscht wurde, und ein Ergebnis von None, wenn der Rc<T>-Wert gelöscht wurde. Da upgrade ein Option<Rc<T>> zurückgibt, wird Rust sicherstellen, dass der Some-Fall und der None-Fall behandelt werden und dass es keinen ungültigen Zeiger gibt.

Als Beispiel werden wir statt einer Liste, deren Elemente nur über das nächste Element wissen, einen Baum erstellen, dessen Elemente über ihre Kind-Elemente und ihre Eltern-Elemente wissen.

Erstellen einer Baum-Datenstruktur: Ein Knoten mit Kindknoten

Zunächst werden wir einen Baum mit Knoten bauen, die über ihre Kindknoten wissen. Wir werden eine Struktur namens Node erstellen, die ihren eigenen i32-Wert sowie Referenzen auf ihre Kind-Node-Werte enthält:

Dateiname: src/main.rs

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

Wir möchten, dass ein Node seine Kinder besitzt und diese Eigentumsverteilung mit Variablen teilen kann, damit wir direkt auf jeden Node im Baum zugreifen können. Dazu definieren wir die Vec<T>-Elemente als Werte vom Typ Rc<Node>. Wir möchten auch ändern können, welche Knoten Kinder eines anderen Knotens sind, daher haben wir eine RefCell<T> in children um die Vec<Rc<Node>>.

Als nächstes werden wir unsere Strukturdefinition verwenden und eine Node-Instanz namens leaf mit dem Wert 3 und keinen Kindern sowie eine andere Instanz namens branch mit dem Wert 5 und leaf als einem seiner Kinder erstellen, wie in Listing 15-27 gezeigt.

Dateiname: src/main.rs

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)]),
    });
}

Listing 15-27: Erstellen eines leaf-Knotens ohne Kinder und eines branch-Knotens mit leaf als einem seiner Kinder

Wir klonen die Rc<Node> in leaf und speichern sie in branch, was bedeutet, dass der Node in leaf jetzt zwei Besitzer hat: leaf und branch. Wir können von branch zu leaf über branch.children gelangen, aber es gibt keinen Weg, von leaf zu branch zu gelangen. Der Grund ist, dass leaf keine Referenz auf branch hat und nicht weiß, dass sie verwandt sind. Wir möchten, dass leaf weiß, dass branch sein Elternknoten ist. Wir werden das nächst tun.

Hinzufügen einer Referenz von einem Kind zu seinem Elternteil

Um das Kindknoten auf seinen Elternteil aufmerksam zu machen, müssen wir ein parent-Feld zu unserer Node-Strukturdefinition hinzufügen. Das Problem besteht darin, zu entscheiden, welchen Typ parent haben sollte. Wir wissen, dass es keinen Rc<T> enthalten kann, da dies einen Referenzzyklus mit leaf.parent auf branch und branch.children auf leaf erzeugen würde, was dazu führen würde, dass ihre strong_count-Werte niemals 0 werden.

Wenn wir die Beziehungen auf eine andere Weise betrachten, sollte ein Elternknoten seine Kinder besitzen: Wenn ein Elternknoten gelöscht wird, sollten auch seine Kindknoten gelöscht werden. Ein Kind sollte jedoch nicht seinen Elternteil besitzen: Wenn wir einen Kindknoten löschen, sollte der Elternteil weiterhin existieren. Dies ist ein Fall für schwache Referenzen!

Wir werden daher statt Rc<T> den Typ von parent auf Weak<T> setzen, speziell auf RefCell<Weak<Node>>. Unsere Node-Strukturdefinition sieht jetzt so aus:

Dateiname: 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>>>,
}

Ein Knoten wird in der Lage sein, auf seinen Elternknoten zu verweisen, aber er besitzt nicht seinen Elternteil. In Listing 15-28 aktualisieren wir main, um diese neue Definition zu verwenden, sodass der leaf-Knoten einen Weg hat, auf seinen Elternteil branch zu verweisen.

Dateiname: src/main.rs

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
      1 parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

  2 println!(
        "leaf parent = {:?}",
        leaf.parent.borrow().upgrade()
    );

    let branch = Rc::new(Node {
        value: 5,
      3 parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

  4 *leaf.parent.borrow_mut() = Rc::downgrade(&branch);

  5 println!(
        "leaf parent = {:?}",
        leaf.parent.borrow().upgrade()
    );
}

Listing 15-28: Ein leaf-Knoten mit einer schwachen Referenz auf seinen Elternknoten, branch

Das Erstellen des leaf-Knotens sieht ähnlich aus wie in Listing 15-27, mit der Ausnahme des parent-Felds: leaf beginnt ohne Elternteil, daher erstellen wir eine neue, leere Weak<Node>-Referenzinstanz [1].

Zu diesem Zeitpunkt erhalten wir, wenn wir versuchen, eine Referenz auf den Elternteil von leaf mithilfe der upgrade-Methode zu erhalten, einen None-Wert. Wir sehen dies in der Ausgabe der ersten println!-Anweisung [2]:

leaf parent = None

Wenn wir den branch-Knoten erstellen, wird auch in seinem parent-Feld eine neue Weak<Node>-Referenz erstellt [3], da branch keinen Elternknoten hat. Wir haben immer noch leaf als eines der Kinder von branch. Sobald wir die Node-Instanz in branch haben, können wir leaf ändern, um ihm eine Weak<Node>-Referenz auf seinen Elternteil zu geben [4]. Wir verwenden die borrow_mut-Methode auf der RefCell<Weak<Node>> im parent-Feld von leaf, und dann verwenden wir die Rc::downgrade-Funktion, um eine Weak<Node>-Referenz auf branch aus der Rc<Node> in branch zu erstellen.

Wenn wir den Elternteil von leaf erneut ausgeben [5], erhalten wir diesmal eine Some-Variante, die branch enthält: jetzt kann leaf auf seinen Elternteil zugreifen! Wenn wir leaf ausgeben, vermeiden wir auch den Zyklus, der letztendlich in einem Stacküberlauf endete, wie wir es in Listing 15-26 hatten; die Weak<Node>-Referenzen werden als (Weak) ausgegeben:

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

Das Fehlen einer unendlichen Ausgabe zeigt an, dass dieser Code keinen Referenzzyklus erzeugt hat. Wir können dies auch daran erkennen, indem wir uns die Werte ansehen, die wir von Rc::strong_count und Rc::weak_count erhalten.

Visualisierung von Änderungen an strong_count und weak_count

Schauen wir uns an, wie sich die strong_count- und weak_count-Werte der Rc<Node>-Instanzen ändern, indem wir einen neuen inneren Bereich erstellen und das Erstellen von branch in diesen Bereich verschieben. Dadurch können wir sehen, was passiert, wenn branch erstellt wird und dann gelöscht wird, wenn es außerhalb des Bereichs geht. Die Änderungen sind in Listing 15-29 gezeigt.

Dateiname: src/main.rs

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

  1 println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );

  2 {
        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);

      3 println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch),
        );

      4 println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf),
        );
  5 }

  6 println!(
        "leaf parent = {:?}",
        leaf.parent.borrow().upgrade()
    );
  7 println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    );
}

Listing 15-29: Erstellen von branch in einem inneren Bereich und Untersuchung von starken und schwachen Referenzzählungen

Nachdem leaf erstellt wurde, hat seine Rc<Node> eine starke Referenzzählung von 1 und eine schwache Referenzzählung von 0 [1]. Im inneren Bereich [2] erstellen wir branch und verbinden es mit leaf. Zu diesem Zeitpunkt, wenn wir die Referenzzählungen ausgeben [3], hat die Rc<Node> in branch eine starke Referenzzählung von 1 und eine schwache Referenzzählung von 1 (für leaf.parent, das auf branch mit einer Weak<Node> verweist). Wenn wir die Referenzzählungen in leaf ausgeben [4], werden wir sehen, dass es eine starke Referenzzählung von 2 hat, da branch jetzt eine Kopie der Rc<Node> von leaf in branch.children gespeichert hat, aber immer noch eine schwache Referenzzählung von 0.

Wenn der innere Bereich endet [5], geht branch außerhalb des Bereichs und die starke Referenzzählung der Rc<Node> sinkt auf 0, sodass seine Node gelöscht wird. Die schwache Referenzzählung von 1 von leaf.parent hat keinen Einfluss darauf, ob die Node gelöscht wird oder nicht, daher erhalten wir keine Speicherlecks!

Wenn wir versuchen, auf den Elternteil von leaf nach dem Ende des Bereichs zuzugreifen, erhalten wir erneut None [6]. Am Ende des Programms [7] hat die Rc<Node> in leaf eine starke Referenzzählung von 1 und eine schwache Referenzzählung von 0, da die Variable leaf jetzt wieder die einzige Referenz auf die Rc<Node> ist.

Alle Logik, die die Referenzzählungen und das Löschen von Werten verwaltet, ist in Rc<T> und Weak<T> und deren Implementierungen des Drop-Traits eingebaut. Indem Sie in der Definition von Node angeben, dass die Beziehung von einem Kind zu seinem Elternteil eine Weak<T>-Referenz sein sollte, können Sie Elternknoten auf Kindknoten und umgekehrt verweisen, ohne einen Referenzzyklus und Speicherlecks zu erzeugen.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "Reference Cycles Can Leak Memory" abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.