![]() |
![]() |
|
![]() |
![]() |
Home Kontakt Links English |
|
C++: Speicherverwaltung mit Smart- und Weak-PointernDas ProblemInnerhalb einer Multithreading-Umgebung ist es schwierig sicherzustellen, dass ein Objekt beim Zugriff existiert. Ein weiteres Problem ist sicherzustellen, dass ein Objekt erst dann zerstört wird, wenn es nicht mehr benötigt wird. Andere Programmiersprachen wie z.B. Java stellen deswegen sogenannte Handles und einen Garbage Collector (GC) zur Verfügung. Aber diese Lösung hat einige Nachteile. So macht Java z.B. keine Vorhersage, wann ein Objekt destruiert wird oder ob es überhaupt jemals geschieht. Ein anderes Problem ist, das GCs nicht in Echtzeit-Umgebungen eingesetzt werden können, da das Zeitverhalten einer mit GC ausgerüsteten Sprache nicht vorhersagbar ist. Von C++ ist man es gewöhnt den Zeitpunkt der Destruktion eines auf dem Stack liegenden Objekts genau zu kennen. Leider bietet die Sprache aber kein solches Feature für Objekte, die auf dem Heap liegen. Deswegen wird eine Technik benötigt, die diese Fähigkeit für alle Objekte bietet. Die Referenzzählungs-Technik ist ein guter Kandidat für solche Zwecke. Sie bietet ein vorhersagbares Zeitverhalten und ist deswegen für Echtzeit-Programme eine gute Wahl. Allerdings ist sie langsamer und bringt ein spezielles neues Problem mit sich - sie kommt mit zirkulären Referenzen schlecht klar. Aber man kann ja nun mal nicht alles bekommen. Smart-PointerEin grundlegender Ansatz ist eine Basisklasse für referenzgezählte Objekte zu erstellen und eine so genannte Smart-Pointer-Klasse anzubieten. Wann immer eine Instanz einer referenzgezählten Klasse erzeugt wird, wird ein Smart-Pointer zurückgegeben, der als Handle fungiert. Man kann Kopien eines Smart-Pointer-Wertes (also der Referenz) anlegen und dem Smart-Pointer neue Werte zuweisen. Es sollte auch möglich sein, Smart-Pointer-Werte zwischen Threads auszutauschen. In jedem Fall ist aber das Resultat solcher Aktionen immer nur die Erzeugung von Kopien der Referenzen und nicht die Vervielfältigung der referenzierten Objekte. Die Zahl der Referenzen wird im referenzierten Objekt abgelegt. Erlischt die letzte Referenz, dann erreicht die Referenzanzahl den Wert 0. Dies führt dann zur sofortigen Destruktion des referenzierten Objekts. Aber was passiert, wenn zwei Objekte sich gegenseitig referenzieren? Dann wird die Referenzanzahl niemals den Wert 0 erreichen und die Technik versagt (siehe Bild unten). Die Lösung dieses Problems liegt in der Unterbrechung der kreisförmigen Kette von Referenzen durch eine schwache (weak) Referenz. Dazu muss einer der Smart-Pointer in dieser Kette durch einen schwächeren Weak-Pointer ersetzt werden. Weak-PointerEin Weak-Pointer verhindert nicht die Destruktion des referenzierten Objekts, falls die letzte Smart-Pointer-Referenz erlischt. Statt dessen bietet er lediglich die Möglichkeit, einen neuen Smart-Pointer aus ihm heraus zu erzeugen, solange das entsprechende Objekt noch verfügbar ist. Zu diesem Zweck muss der Weak-Pointer in der Lage sein zu prüfen, ob das referenzierte Objekte noch verfügbar ist. Es gibt mindestens zwei Wege dies zu implementieren. Im ersten Fall wird zusätzlich zur Smart-Referenzanzahl die Weak-Referenzanzahl zur referenzgezählten Klasse hinzugefügt. Erreicht die Smart-Referenzanzahl den Wert 0, dann muss der letzte Smart-Pointer eine spezielle finalize-Methode auf dem Objekt aufrufen. In der finalize-Methode hat das Objekt alle seine Referenzen auf null zu setzen, so dass alle abhängigen Objekte ebenfalls finalisiert werden. Dies würde zu einer Destruktion aller verbliebenen Weak-Pointer führen und sobald die Anzahl der Weak-Referenzen ebenfalls 0 erreicht, könnte das Objekt destruiert werden. Anhand des unteren Bildes soll ein detailliertes Beispiel durchgespielt werden. Zwei Objekte werden jeweils durch einen einzigen Smart-Pointer referenziert und das zweite Objekt hat einen Weak-Pointer, der auf das erste Objekt verweist. Sollte der Smart-Pointer 1 auf null gesetzt werden oder selbst destruiert werden, dann würde die Smart-Referenzahzahl von Objekt 1 auf 0 reduziert. Dies wird vom Smart-Pointer 1 erkannt, wobei es jedoch sehr wichtig ist, dass er vor der Reduzierung erst die finalize-Methode von Objekt 1 aufruft. Nun werden in dieser finalize-Methode alle Pointer auf null gesetzt. Demzufolge muss die Smart-Referenzanzahl von Objekt 2 durch den Smart-Pointer 2 um eins vermindert werden und würde deshalb ebenfalls 0 erreichen. Smart-Pointer 2 erkennt dies vor der Dekrementierung und ruft deswegen vorher die finalize-Methode von Objekt 2 auf. In der finalize-Methode von Objekt 2 wiederum werden alle Pointer auf null gesetzt - also auch die Weak-Pointer. Als Folge davon erniedrigt der Weak-Pointer 1 die Weak-Referenzanzahl von Objekt 1 um 1 auf 0. Dabei stellt der Weak-Pointer 1 vorher fest, dass er das Objekt 1 nicht zu destruieren hat, weil die Smart-Referenzanzahl immer noch 1 ist (Sie erinnern sich, dass die Smart-Referenzanzahl erst nach dem Aufruf der finalize-Methode dekrementiert wird - und wir befinden uns immer noch in einem solchen Aufruf, richtig?). Nun kehrt der Aufruf von finalize im Objekt 2 zurück und Smart-Pointer 2 vermindert die Smart-Referenzanzahl dieses Objekts um 1. Er stellt fest, dass nicht nur die Smart-Referenzanzahl den Wert 0 erreicht hat, sondern auch die Weak-Referenanzahl auf 0 steht. Also ruft er nun delete für Objekt 2 auf was zur Destruktion und Freigabe des von Objekt 2 belegten Speichers führt. Jetzt kehrt der Aufruf von finalize im Objekt 1 ebenfalls zurück und Smart-Pointer 1 vermindert die Smart-Referenzanzahl dieses Objekts um 1. Er ruft delete für Objekt 1 auf, weil die Weak-Referenzanzahl 0 erreicht hat. Und so wird der gesamte von den beiden Objekten belegte Speicher freigegeben, obwohl wir eine zirkuläre Referenzierung hatten.
Diese Technik habe ich bereits mit guten Erfolgen eingesetzt, aber die Lösung hat einen Nachteil ... der vom referenzierten Objekt belegte Speicher bleibt belegt solange die Weak-Referenzanzahl nicht den Wert 0 erreicht. Der Grund dafür ist, dass ein Smart-Pointer nicht die Weak-Pointer des referenzierten Objekts zurücksetzen kann. Wenn ein großes Objekt benutzt wird, ist dies sicher nicht optimal. Die Lösung ist, alle Weak-Pointer eines referenzierten Objekts doppelt zu verlinken. Dies würde zwar zu einer Verdreifachung des Speicherbedarfs für Weak-Pointer führen, aber per Definition sind Weak-Pointer ja sowieso nur selten einzusetzen. Natürlich gibt es noch einen weiteren Nachteil - die Stack-Größe, die für die Destruktion großer Objekt-Graphen benötigt wird, ist groß. Aber falls dies zum Problem wird, denken Sie bitte daran, dass jeder rekursiv arbeitende Algorithmus in einen nicht rekursiv arbeitenden Algorithmus transformiert werden kann. Copyright (c) 2016: Jürgen Luers |