Ist nicht deterministisches Ressourcenmanagement eine undichte Abstraktion?

9

Soweit ich sehen kann, gibt es zwei allgegenwärtige Formen des Ressourcenmanagements: deterministische Zerstörung und explizite. Beispiele für Ersteres wären C ++ - Destruktoren und intelligente Zeiger oder Perls DESTROY-Sub, während ein Beispiel für Letzteres Rubys Block-to-Management-Ressourcen-Paradigma oder die IDispose-Schnittstelle von .NET wäre.

Neuere Sprachen scheinen sich für Letzteres zu entscheiden, möglicherweise als Nebeneffekt der Verwendung von Müllsammelsystemen der Sorte ohne Referenzzählung.

Meine Frage lautet: Angesichts der Tatsache, dass Destruktoren für intelligente Zeiger oder Speicherbereinigungssysteme mit Referenzzählung - fast dasselbe - implizite und transparente Zerstörung von Ressourcen ermöglichen, ist dies eine weniger undichte Abstraktion als die nicht deterministischen Typen, die auf expliziten Daten beruhen Notation?

Ich werde ein konkretes Beispiel geben. Wenn Sie drei C ++ - Unterklassen einer einzelnen Oberklasse haben, verfügt eine möglicherweise über eine Implementierung, die keine spezifische Zerstörung benötigt. Vielleicht macht es seine Magie auf andere Weise. Die Tatsache, dass keine besondere Zerstörung erforderlich ist, spielt keine Rolle - alle Unterklassen werden weiterhin auf die gleiche Weise verwendet.

Ein anderes Beispiel verwendet Ruby-Blöcke. Zwei Unterklassen müssen Ressourcen freigeben, daher entscheidet sich die Oberklasse für eine Schnittstelle, die einen Block im Konstruktor verwendet, obwohl andere spezifische Unterklassen diesen möglicherweise nicht benötigen, da sie keine besondere Zerstörung erfordern.

Ist es der Fall, dass letztere Implementierungsdetails der Ressourcenzerstörung preisgibt, während dies bei ersteren nicht der Fall ist?

EDIT: Ruby mit Perl zu vergleichen, könnte fairer sein, da einer eine deterministische Zerstörung hat und der andere nicht, aber beide sind Müllsammler.

Louis Jackman
quelle
5
Ich bin versucht, "Ja" zu sagen, aber ich höre gerne, was andere dazu zu sagen haben.
Bart van Ingen Schenau
Transparente Ressourcenzerstörung? Abgesehen von der Tatsache, dass Sie intelligente Zeiger anstelle von normalen Zeigern verwenden müssen? Ich denke nicht, dass dies transparenter ist, als nur einen Mechanismus (Referenzen) für den Zugriff auf Objekte zu haben (in C ++ haben Sie mindestens vier oder fünf).
Giorgio
@Giorgio: "Möglichkeiten, auf ein Objekt zuzugreifen" ist ziemlich vage. Meinst du lesen oder schreiben? Const / Volatile Qualifikation? Zeiger sind nicht wirklich "eine Möglichkeit, auf ein Objekt zuzugreifen"; So ziemlich jeder Ausdruck führt zu einem Objekt und die Dereferenzierung eines Zeigers ist einfach nichts Besonderes.
MSalters
1
@Giorgio: In diesem OOP-Sinne können Sie keine Nachricht an einen C ++ - Zeiger senden. Sie müssen den Zeiger dereferenzieren, um die Nachricht zu senden, (*ptr).Message()oder gleichwertig ptr->Message(). Es gibt unendlich viele erlaubte Ausdrücke, wie ((*ptr))->Messagees auch äquivalent ist. Aber sie alle expressionIdentifyingAnObject.Message()
laufen
1
Beim Nachzählen müssen Sie vorsichtig sein, um Kreise zu vermeiden. Diese Abstraktion leckt also auch, nur auf eine andere Art und Weise.
CodesInChaos

Antworten:

2

Ihr eigenes Beispiel beantwortet die Frage. Die transparente Zerstörung ist deutlich weniger undicht als die explizite Zerstörung. Es kann auslaufen, ist aber weniger undicht.

Explizite Zerstörung ist analog zu malloc / free in C mit allen Fallstricken. Vielleicht mit etwas syntaktischem Zucker, um es bereichsabhängig erscheinen zu lassen.

Einige der Vorteile der transparenten Zerstörung gegenüber expliziten: -
Gleiches Nutzungsmuster -
Sie können nicht vergessen, die Ressource freizugeben.
- Aufräumen von Details verschmutzt die Landschaft am Verwendungsort nicht.

mike30
quelle
2

Das Versagen in der Abstraktion ist eigentlich nicht die Tatsache, dass die Speicherbereinigung nicht deterministisch ist, sondern die Idee, dass Objekte an Dingen "interessiert" sind, auf die sie sich beziehen, und nicht an Dingen, an denen sie nicht festhalten Verweise. Um zu sehen, warum, betrachten Sie das Szenario eines Objekts, das einen Zähler dafür enthält, wie oft ein bestimmtes Steuerelement gezeichnet wird. Bei der Erstellung wird das "Paint" -Ereignis des Steuerelements abonniert und bei der Entsorgung abgemeldet. Das Klickereignis erhöht einfach ein Feld und eine Methode getTotalClicks()gibt den Wert dieses Feldes zurück.

Wenn das Zählerobjekt erstellt wird, muss es einen Verweis auf sich selbst in der Steuerung speichern, die es überwacht. Das Steuerelement kümmert sich wirklich nicht um das Zählerobjekt und wäre genauso glücklich, wenn das Zählerobjekt und der Verweis darauf nicht mehr existieren würden, aber solange der Verweis existiert, ruft es jedes Mal den Ereignishandler dieses Objekts auf es malt sich. Diese Aktion ist für die Steuerung völlig nutzlos, wäre jedoch für jeden nützlich, der getTotalClicks()das Objekt jemals aufrufen würde .

Wenn beispielsweise eine Methode ein neues "Farbzähler" -Objekt erstellen, eine Aktion für das Steuerelement ausführen, beobachten würde, wie oft das Steuerelement neu gestrichen wurde, und dann das Farbzählerobjekt verlassen würde, würde das Objekt das Ereignis sogar abonniert bleiben obwohl es niemanden interessieren würde, wenn das Objekt und alle Verweise darauf einfach verschwinden würden. Die Objekte können jedoch erst erfasst werden, wenn das Steuerelement selbst aktiviert ist. Wenn die Methode innerhalb der Lebensdauer des Steuerelements viele tausend Mal aufgerufen würde [ein plausibles Szenario], könnte dies zu einem Speicherüberlauf führen, wenn jedoch die Kosten für N Aufrufe wahrscheinlich O (N ^ 2) oder O betragen würden (N ^ 3) es sei denn, die Abonnementverarbeitung war sehr effizient und die meisten Vorgänge umfassten tatsächlich kein Malen.

Dieses spezielle Szenario könnte dadurch gelöst werden, dass die Steuerung einen schwachen Bezug zum Gegenobjekt behält und keinen starken. Ein schwaches Abonnementmodell ist hilfreich, funktioniert aber im allgemeinen Fall nicht. Angenommen, anstatt ein Objekt zu haben, das eine einzelne Art von Ereignis von einem einzelnen Steuerelement aus überwacht, möchte man ein Ereignisprotokollierungsobjekt haben, das mehrere Steuerelemente überwacht, und der Mechanismus zur Ereignisbehandlung des Systems war so, dass jedes Steuerelement eine Referenz benötigte zu einem anderen Ereignis-Logger-Objekt. In diesem Fall sollte das Objekt, das ein Steuerelement mit dem Ereignisprotokollierer verknüpft, nur so lange aktiv bleiben, wie beideDie zu überwachende Steuerung und der Ereignisprotokollierer bleiben nützlich. Wenn weder das Steuerelement noch der Ereignisprotokollierer einen starken Verweis auf das Verknüpfungsereignis enthalten, wird es nicht mehr existieren, obwohl es immer noch "nützlich" ist. Wenn eines der beiden Ereignisse ein starkes Ereignis enthält, kann die Lebensdauer des Verknüpfungsobjekts unbrauchbar verlängert werden, selbst wenn das andere stirbt.

Wenn irgendwo im Universum kein Verweis auf ein Objekt existiert, kann das Objekt sicher als nutzlos betrachtet und aus der Existenz ausgeschlossen werden. Die Tatsache, dass ein Verweis auf ein Objekt existiert, bedeutet jedoch nicht, dass das Objekt "nützlich" ist. In vielen Fällen hängt die tatsächliche Nützlichkeit von Objekten von der Existenz von Verweisen auf andere Objekte ab, die aus GC-Sicht völlig unabhängig von ihnen sind.

Wenn Objekte deterministisch benachrichtigt werden, wenn niemand an ihnen interessiert ist, können sie diese Informationen verwenden, um sicherzustellen, dass jeder, der von diesem Wissen profitieren würde, informiert wird. In Ermangelung einer solchen Benachrichtigung gibt es jedoch keine allgemeine Möglichkeit zu bestimmen, welche Objekte als "nützlich" angesehen werden, wenn man nur die Menge der vorhandenen Referenzen kennt und nicht die semantische Bedeutung, die diesen Referenzen zugeordnet ist. Somit wäre jedes Modell, das davon ausgeht, dass das Vorhandensein oder Nichtvorhandensein von Referenzen für ein automatisiertes Ressourcenmanagement ausreicht, zum Scheitern verurteilt, selbst wenn der GC die Aufgabe eines Objekts sofort erkennen könnte.

Superkatze
quelle
0

Nein, "der Destruktor oder eine andere Schnittstelle, die besagt, dass" diese Klasse zerstört werden muss ", ist ein Vertrag dieser Schnittstelle. Wenn Sie einen Subtyp erstellen, der keine besondere Zerstörung erfordert, würde ich dies als Verstoß gegen das Liskov-Substitutionsprinzip betrachten .

Was C ++ und andere betrifft, gibt es keinen großen Unterschied. C ++ erzwingt diese Schnittstelle für alle Objekte. Abstraktionen können nicht auslaufen, wenn sie von der Sprache benötigt werden.

Telastyn
quelle
4
"Wenn Sie einen Subtyp erstellen, der keine besondere Zerstörung erfordert" Dies ist keine LSP-Verletzung, da no-op ein gültiger Sonderfall der Zerstörung ist. Das Problem besteht darin, dass Sie die Zerstörungsanforderung einer abgeleiteten Klasse hinzufügen.
CodesInChaos
Ich werde hier verwirrt. Wenn einer C ++ - Unterklasse spezieller Zerstörungscode hinzugefügt werden muss, werden die Verwendungsmuster überhaupt nicht geändert, da dies automatisch erfolgt. Das heißt, die Oberklasse und die Unterklasse können weiterhin austauschbar verwendet werden. Aber mit expliziter Notation für das Ressourcenmanagement würde eine Unterklasse, die explizite Zerstörung benötigt, ihre Verwendung mit einer Oberklasse inkompatibel machen, nicht wahr? (Vorausgesetzt, die Superklasse brauchte keine explizite Zerstörung.)
Louis Jackman
@CodesInChaos - ah ja, ich nehme an, das stimmt.
Telastyn
@ljackman: Eine Klasse, die eine besondere Zerstörung erfordert, belastet jeden, der seinen Konstruktor anruft, um sicherzustellen, dass sie ausgeführt wird. Dies erzeugt keine LSP-Verletzung, da a DerivedFooThatRequiresSpecialDestructionnur durch Code erstellt werden kann, der aufruft new DerivedFooThatRequiresSpecialDestruction(). Andererseits wäre eine Factory-Methode, die einen DerivedFooThatRequiresSpecialDestructionTo-Code zurückgibt, der nicht erwartet, dass etwas zerstört werden muss, eine LSP-Verletzung.
Supercat
0

Meine Frage lautet: Angesichts der Tatsache, dass Destruktoren für intelligente Zeiger oder Speicherbereinigungssysteme mit Referenzzählung - fast dasselbe - implizite und transparente Zerstörung von Ressourcen ermöglichen, ist dies eine weniger undichte Abstraktion als die nicht deterministischen Typen, die auf expliziten Daten beruhen Notation?

Von Hand auf Zyklen achten zu müssen, ist weder implizit noch transparent. Die einzige Ausnahme ist ein Referenzzählsystem mit einer Sprache, die Zyklen von Natur aus verbietet. Erlang könnte ein Beispiel für ein solches System sein.

Beide Ansätze lecken also. Der Hauptunterschied besteht darin, dass Destruktoren in C ++ überall auslaufen, in IDispose.NET jedoch sehr selten sind.

Jon Harrop
quelle
1
Außer Zyklen sind äußerst selten und treten praktisch nie auf, außer in Datenstrukturen, die explizit zyklisch sind. Der Hauptunterschied besteht darin, dass Destruktoren in C ++ überall richtig behandelt werden, IDispose das Problem in .NET jedoch selten behandelt.
DeadMG
"Außer Zyklen sind außerordentlich selten". In modernen Sprachen? Ich würde diese Hypothese in Frage stellen.
Jon Harrop