Wie behandelt man das Fehlschlagen der Freigabe einer Ressource, die in einem Smart Pointer enthalten ist?

8

Wie sollte ein Fehler bei der Ressourcenfreigabe behandelt werden, wenn der Objekt, das die Ressource darstellt, ist in einem gemeinsamen Zeiger enthalten?

EDIT 1:

  

Um diese Frage konkreter zu formulieren: Viele C-Style-Interfaces   eine Funktion haben, um eine Ressource zuzuordnen, und eine, die freigegeben werden soll   es. Beispiele sind open (2) und close (2) für Dateideskriptoren in POSIX   systems, XOpenDisplay und XCloseDisplay für eine Verbindung zu einem X   Server oder sqlite3_open und sqlite3_close für eine Verbindung zu einem   SQLite-Datenbank.

     

Ich mag solche Schnittstellen in einer C ++ - Klasse unter Verwendung der Pimpl kapseln   Idiom, um die Implementierungsdetails zu verbergen und eine Fabrik zur Verfügung zu stellen   Methode, die einen gemeinsamen Zeiger zurückgibt, um sicherzustellen, dass die Ressource ist   freigegeben, wenn keine Referenzen mehr vorhanden sind.

     

Aber, in allen oben angegebenen Beispielen und vielen anderen, die Funktion   verwendet, um die Ressource freizugeben, kann einen Fehler melden. Wenn diese Funktion ist   Vom Destruktor aufgerufen, kann ich keine Ausnahme werfen, weil   allgemein Destruktoren dürfen nicht werfen.

     

Wenn ich andererseits eine öffentliche Methode zur Freigabe der   Ressource, ich habe jetzt eine Klasse mit zwei möglichen Zuständen: Einer, in dem   Die Ressource ist gültig und eine, in der die Ressource bereits vorhanden ist   freigegeben. Dies erschwert nicht nur die Umsetzung der   Klasse, es öffnet auch ein Potenzial für die falsche Verwendung. Das ist schlecht, weil   Eine Schnittstelle sollte darauf abzielen, Benutzungsfehler unmöglich zu machen.

     

Ich wäre dankbar für jede Hilfe bei diesem Problem.

     

Die ursprüngliche Aussage der Frage und Gedanken über ein mögliches   Lösung folgen unten.

EDIT 2:

  

Es gibt jetzt eine Belohnung für diese Frage. Eine Lösung muss diese erfüllen   Anforderungen:

     
  1. Die Ressource wird genau dann freigegeben, wenn keine Verweise darauf verbleiben.
  2.   
  3. Verweise auf die Ressource können explizit gelöscht werden. Eine Ausnahme wird ausgelöst, wenn beim Freigeben der Ressource ein Fehler aufgetreten ist.
  4.   
  5. Es ist nicht möglich, eine Ressource zu verwenden, die bereits freigegeben wurde.
  6.   
  7. Referenzzählung und Freigabe der Ressource sind Thread-sicher .
  8.   

Eine Lösung sollte diese Anforderungen erfüllen:

     
  1. Es verwendet den gemeinsamen Zeiger, der von boost bereitgestellt wird, das C ++ Technischer Bericht 1 (TR1) und der kommende C ++ - Standard, C++0x .
  2.   
  3. Es ist generisch. Ressourcenklassen müssen nur implementieren, wie die Ressource freigegeben wird.
  4.   

Danke für Ihre Zeit und Ihre Gedanken.

EDIT 3:

  

Danke an alle, die meine Frage beantwortet haben.

     

Alsk Antwort erfüllt alles, um in der Prämie gefragt, und   wurde akzeptiert. In Multithread-Code würde diese Lösung erfordern   ein separater Cleanup-Thread.

     

Ich habe eine andere Antwort wo irgendwelche Ausnahmen während   Cleanup werden von dem Thread geworfen, der die Ressource tatsächlich verwendet hat,   ohne Notwendigkeit eines separaten Aufräumfadens. Wenn du still bist   Interesse an diesem Problem (es stört mich sehr), bitte   Kommentar.

Intelligente Zeiger sind ein nützliches Werkzeug, um Ressourcen sicher zu verwalten. Beispiele von solchen Ressourcen sind Speicher, Festplattendateien, Datenbankverbindungen oder Netzwerkverbindungen.

%Vor%

In einem typischen Szenario sollte die Klasse, die die Ressource einkapselt, sein nicht kopierbar und polymorph. Eine gute Möglichkeit, dies zu unterstützen, ist die Bereitstellung Eine Factory-Methode, die einen gemeinsamen Zeiger zurückgibt und alles deklariert Konstruktoren nicht öffentlich. Die geteilten Zeiger können jetzt kopiert werden und zugewiesen zu frei. Das Objekt wird automatisch zerstört, wenn nicht Verweis auf es bleibt, und der Destruktor gibt dann die Ressource.

%Vor%

Aber es gibt ein Problem mit diesem Ansatz. Der Destruktor darf nicht werfen, so dass ein Fehler beim Freigeben der Ressource unentdeckt bleibt.

Ein gängiger Ausweg aus diesem Problem besteht darin, eine öffentliche Methode zur Freigabe hinzuzufügen die Ressource.

%Vor%

Leider bringt dieser Ansatz ein anderes Problem mit sich: Unsere Objekte Kann nun Ressourcen enthalten, die bereits freigegeben wurden. Dies erschwert die Implementierung der Ressourcenklasse. Schlimmer noch, es ermöglicht es Clients der Klasse, sie falsch zu verwenden. Das folgendes Beispiel scheint weit hergeholt, aber es ist eine gemeinsame Falle Multi-Thread-Code.

%Vor%

Entweder stellen wir sicher, dass die Ressource nicht vor dem Objekt freigegeben wird wird zerstört, wodurch jeder Weg verloren geht, mit einer gescheiterten Ressource umzugehen Freigabe Oder wir bieten eine Möglichkeit, die Ressource explizit freizugeben während der Lebensdauer des Objekts, wodurch es möglich gemacht wird, die Ressourcenklasse nicht korrekt.

Es gibt einen Ausweg aus diesem Dilemma. Aber die Lösung beinhaltet die Verwendung eines modifizierte gemeinsame Zeigerklasse. Diese Änderungen werden wahrscheinlich sein umstritten.

Typische gemeinsame Zeigerimplementierungen, z. B. boost :: shared_ptr, erfordern, dass keine Ausnahme ausgelöst wird, wenn der Destruktor ihres Objekts ist namens. Im Allgemeinen sollte kein Destruktor jemals werfen, also ist dies ein angemessene Anforderung. Diese Implementierungen ermöglichen auch eine benutzerdefinierte Methode Deletierfunktion anzugeben, die anstelle der Destruktor, wenn keine Referenz auf das Objekt verbleibt. Der Nicht-Wurf Anforderung wird auf diese benutzerdefinierte Löschfunktion erweitert.

Die Begründung für diese Anforderung ist klar: Der gemeinsame Zeiger ist Destruktor darf nicht werfen. Wenn die Löschfunktion nicht ausgelöst wird, noch wird der Destruktor des gemeinsamen Zeigers. Das Gleiche gilt jedoch für andere Mitgliedsfunktionen des geteilten Zeigers, die zur Ressource führen Deallokation, z.B. reset (): Wenn die Ressourcenfreigabe fehlschlägt, nein Ausnahme kann ausgelöst werden.

Die hier vorgeschlagene Lösung ermöglicht benutzerdefinierte Löschfunktionen werfen. Dies bedeutet, dass der Destruktor des gemeinsam genutzten Pointers geändert werden muss Ausnahmen abfangen, die von der Löschfunktion ausgelöst werden. Andererseits, andere Elementfunktionen als der Destruktor, z.B. reset (), soll nicht Ausnahmen von der Deleter-Funktion (und deren Implementierung) abfangen wird etwas komplizierter).

Hier ist das ursprüngliche Beispiel mit einer Löschfunktion:

%Vor%

Wir können jetzt reset () verwenden, um die Ressource explizit freizugeben. Wenn da ist immer noch ein Verweis auf die Ressource in einem anderen Thread oder einem anderen Teil von Das Programm, das reset () aufruft, dekrementiert nur die Referenz Anzahl. Wenn dies der letzte Verweis auf die Ressource ist, ist die Ressource freigegeben. Wenn die Ressourcenfreigabe fehlschlägt, wird eine Ausnahme ausgelöst.

%Vor%

BEARBEITEN:

Hier ist eine vollständige (aber plattformabhängige) Implementierung des Deleters:

%Vor%     
cj. 16.05.2010, 19:39
quelle

6 Antworten

4

Wir müssen die zugewiesenen Ressourcen irgendwo speichern (wie bereits von DeadMG ) und rufen Sie explizit einige Reporting / werfen-Funktion außerhalb eines Destruktors. Aber das hindert uns nicht daran, die in boost :: shared_ptr implementierte Referenzzählung zu nutzen.

%Vor%

Die Implementierung von cleanupAndReport () sollte ein wenig komplizierter sein: In der vorliegenden Version wird der Pool nach der Bereinigung mit Nullpointern gefüllt, und im Falle einer Ausnahmebedingung müssen wir die Funktion aufrufen, bis sie nicht mehr geworfen wird , aber ich hoffe, es illustriert die Idee gut.

Nun, allgemeinere Lösung:

%Vor%     
Alsk 24.05.2010, 21:29
quelle
4

Wenn das Freigeben einer Ressource tatsächlich fehlschlagen kann , dann ist ein Destruktor eindeutig eine falsche Abstraktion, die zu verwenden ist. Destruktoren sollen unabhängig von den Umständen fehlerfrei aufgeräumt werden. Eine close() -Methode (oder wie immer Sie es nennen möchten) ist wahrscheinlich die einzige Möglichkeit.

Aber denke genau darüber nach. Wenn die Freigabe einer Ressource tatsächlich fehlschlägt , was können Sie tun? Ist solch ein Fehler wiederherstellbar? Wenn ja, welcher Teil Ihres Codes sollte damit umgehen? Der Weg zur Wiederherstellung ist wahrscheinlich sehr anwendungsspezifisch und an andere Teile der Anwendung gebunden. Es ist sehr unwahrscheinlich, dass Sie dies tatsächlich automatisch an einer beliebigen Stelle im Code erreichen möchten, die die Ressource freigegeben hat und den Fehler ausgelöst hat. Eine gemeinsame Zeigerabstraktion modelliert nicht wirklich, was Sie erreichen möchten. Wenn dies der Fall ist, müssen Sie Ihre eigene Abstraktion erstellen, die Ihr gewünschtes Verhalten modelliert. Gemeinsame Zeiger zu missbrauchen, um etwas zu tun, was sie nicht tun sollten, ist nicht der richtige Weg.

Lesen Sie auch das .

BEARBEITEN:
Wenn Sie lediglich den Benutzer darüber informieren möchten, was vor dem Absturz passiert ist, sollten Sie das Wrapper-Objekt Socket in ein weiteres Wrapper-Objekt einfügen, das den Deleter bei seiner Zerstörung aufruft, alle ausgelösten Exceptions abfängt und behandelt indem Sie dem Benutzer ein Meldungsfeld oder was auch immer zeigen. Dann setze dieses Wrapper-Objekt in ein boost::shared_ptr .

    
slacker 16.05.2010 20:59
quelle
1

Zitat von Herb Sutter, Autor von "Exceptional C ++" (aus hier ):

  

Wenn ein Destruktor eine Ausnahme auslöst,   Schlechte Dinge können passieren. Speziell,   Betrachten Sie den Code wie folgt:

%Vor%
  

Wenn ein Destruktor eine Ausnahme auslöst   während eine andere Ausnahme bereits ist   aktiv (d. h. während des Abwickelns des Stapels),   Das Programm ist beendet. Das ist   normalerweise keine gute Sache.

Mit anderen Worten, unabhängig davon, was Sie glauben möchten, ist in dieser Situation elegant, Sie können nicht einfach eine Exception in einen Destruktor werfen, es sei denn, Sie können garantieren, dass sie nicht ausgelöst wird, wenn Sie eine andere Exception behandeln.

Außerdem, was können Sie tun, wenn Sie eine Ressource nicht erfolgreich loswerden können? Ausnahmen sollten für Dinge ausgelöst werden, die höher behandelt werden können, nicht für Fehler. Wenn Sie merkwürdiges Verhalten melden möchten, protokollieren Sie den Fehler bei der Veröffentlichung und fahren Sie einfach fort. Oder beenden.

    
MSN 24.05.2010 16:57
quelle
1

Wie in der Frage angekündigt, editieren Sie 3:

Hier ist eine andere Lösung, die, soweit ich beurteilen kann, die erfüllt Anforderungen in der Frage. Es ist ähnlich wie die beschriebene Lösung in der ursprünglichen Frage, aber verwendet boost::shared_ptr anstelle von a benutzerdefinierter intelligenter Zeiger.

Die zentrale Idee dieser Lösung ist es, ein release() bereitzustellen Operation auf shared_ptr . Wenn wir das shared_ptr aufgeben können, Eigentum, wir sind frei, eine Bereinigungsfunktion aufzurufen, löschen Sie das Objekt, und eine Ausnahme auslösen, falls während der Bereinigung ein Fehler aufgetreten ist.

Boost hat ein Gut Grund eine Operation release() für shared_ptr nicht bereitstellen:

  

shared_ptr kann den Eigentümer nicht freigeben, es sei denn, es ist eindeutig (), weil der   Eine andere Kopie wird das Objekt trotzdem zerstören.

     

Überlegen Sie:

%Vor%      

Außerdem wäre der von release () zurückgegebene Zeiger schwierig   Zuverlässig freigeben, da die Quelle shared_ptr erstellt werden konnte   mit einem benutzerdefinierten Deleter.

Das erste Argument gegen eine Operation release() ist, dass durch die Art von shared_ptr , viele Zeiger teilen sich den Besitz des Objekts, also kann keiner von ihnen diesen Besitz einfach freigeben. Aber was wenn die Funktion release() einen Nullzeiger zurückgab, wenn es einen gab noch andere Referenzen übrig? Der shared_ptr kann zuverlässig ermitteln dies ohne Rassenbedingungen.

Das zweite Argument gegen die Operation release() ist, dass, wenn a Custom Deleter wurde an die shared_ptr übergeben, die Sie verwenden sollten Die Zuordnung des Objekts wird aufgehoben, anstatt es einfach zu löschen. Aber release() könnte zusätzlich zum rohen Zeiger ein Funktionsobjekt zurückgeben ermöglichen Sie dem Aufrufer, den Zeiger zuverlässig zu entfernen.

In unserem spezifischen Szenario werden benutzerdefinierte Deleters jedoch nicht verwendet Problem, weil wir uns nicht mit willkürlichen Gewohnheiten befassen müssen Löscher. Dies wird aus dem unten angegebenen Code klarer.

Bereitstellung einer Operation release() auf shared_ptr ohne Änderung seine Umsetzung ist natürlich ohne Hack nicht möglich. Das Der Hack, der im folgenden Code verwendet wird, beruht auf einer thread-lokalen Variablen um zu verhindern, dass unser benutzerdefinierter Löschvorgang das Objekt tatsächlich löscht.

Das heißt, hier ist der Code, der hauptsächlich aus dem Header besteht Resource.hpp , plus eine kleine Implementierungsdatei Resource.cpp . Hinweis dass es aufgrund der. mit -lboost_thread-mt verknüpft sein muss Thread-lokale Variable.

%Vor%

Und die Implementierungsdatei:

%Vor%

Und hier ist ein Beispiel für eine Ressourcenklasse, die mit dieser Lösung implementiert wurde:

%Vor%

Schließlich ist hier ein kleines Makefile, um all das aufzubauen:

%Vor%     
cj. 16.06.2010 23:26
quelle
0

Nun, zunächst einmal sehe ich hier keine Frage. Zweitens muss ich sagen, dass dies eine schlechte Idee ist. Was wirst du an all dem bekommen? Wenn der letzte geteilte Zeiger auf eine Ressource zerstört wird und Ihr werfender Löscher aufgerufen wird, finden Sie sich mit einem Ressourcenleck wieder. Sie haben alle Handles für die Ressource verloren, die nicht freigegeben wurde. Du wirst es nie wieder versuchen können.

Ihr Wunsch, ein RAII-Objekt zu verwenden, ist gut, aber ein intelligenter Zeiger reicht für diese Aufgabe einfach nicht aus. Was Sie brauchen, muss noch schlauer sein. Sie benötigen etwas, das sich bei einem Totalausfall wieder aufbauen kann. Der Destruktor ist für eine solche Schnittstelle nicht ausreichend.

Sie stellen sich dem Missbrauch vor, bei dem jemand eine Ressource dazu bringen könnte, einen Handle zu haben, aber ungültig ist. Die Art der Ressource, mit der Sie es hier zu tun haben, eignet sich einfach für dieses Problem. Es gibt viele Möglichkeiten, wie Sie sich diesem nähern können. Eine Methode kann sein, das Griff / Körper-Idiom zusammen mit dem Zustandsmuster zu verwenden. Die Implementierung der Schnittstelle kann in einem von zwei Zuständen erfolgen: verbunden oder nicht verbunden. Der Handle übergibt einfach Anfragen an den internen Körper / Staat. Connected funktioniert wie normal, unconnected löst Ausnahmen / Behauptungen in allen anwendbaren Anfragen aus.

Dieses Ding würde eine andere Funktion als ~ benötigen, um einen Griff zu zerstören. Du könntest eine destroy () Funktion betrachten, die werfen kann. Wenn Sie beim Aufruf einen Fehler bemerken, löschen Sie nicht den Handle, sondern behandeln das Problem auf die jeweils anwendungsspezifische Art und Weise. Wenn Sie keinen Fehler von destroy () erhalten, lassen Sie das Handle außerhalb des Gültigkeitsbereichs, setzen Sie es zurück oder was auch immer. Die Funktion destroy () berechnet dann die Ressourcenanzahl und versucht, die interne Ressource freizugeben, wenn dieser Wert 0 ist. Bei Erfolg wird der Handle in den nicht verbundenen Zustand versetzt, bei einem Fehler wird ein abfangbarer Fehler generiert, den der Client zu verarbeiten versucht der Handle in einem verbundenen Zustand.

Es ist nicht eine ganz triviale Sache zu schreiben, aber was Sie tun wollen, Ausnahmen in die Zerstörung einzuführen, wird einfach nicht funktionieren.

    
Crazy Eddie 16.05.2010 20:04
quelle
0

Wenn die C-Stil-Schließung einer Ressource fehlschlägt, handelt es sich im Allgemeinen um ein Problem mit der API und nicht um ein Problem in Ihrem Code. Was ich jedoch versucht wäre, wenn die Zerstörung fehlgeschlagen ist, füge es zu einer Liste von Ressourcen hinzu, die später erneut zerstört / aufgeräumt werden müssen, zB wenn App regelmäßig oder wenn andere ähnliche Ressourcen zerstört werden, und dann versuche es erneut zu zerstören. Wenn irgendwelche zu einer beliebigen Zeit übrig sind, geben Sie einen Benutzerfehler ein und beenden Sie ihn.

    
Puppy 21.05.2010 20:03
quelle

Tags und Links