JLI Spieleprogrammierung Foren-Übersicht JLI Spieleprogrammierung

 
 FAQFAQ   SuchenSuchen   MitgliederlisteMitgliederliste   BenutzergruppenBenutzergruppen 
 medals.php?sid=1e6eed361ec000d1a708396f9e2dd32bMedaillen   RegistrierenRegistrieren   ProfilProfil   Einloggen, um private Nachrichten zu lesenEinloggen, um private Nachrichten zu lesen   LoginLogin 

Ausnahmesicherer Code

 
Neues Thema eröffnen   Neue Antwort erstellen    JLI Spieleprogrammierung Foren-Übersicht -> Tutorials
Vorheriges Thema anzeigen :: Nächstes Thema anzeigen  
Autor Nachricht
AFE-GmdG
JLI MVP
JLI MVP


Alter: 44
Anmeldedatum: 19.07.2002
Beiträge: 1374
Wohnort: Irgendwo im Universum...
Medaillen: Keine

BeitragVerfasst am: 28.02.2009, 23:25    Titel: Ausnahmesicherer Code Antworten mit Zitat

Ausnahmesicherer Code.

Neulich, als ich jemanden mal wieder eine Frage beantwortet habe, wurde mir unter anderem ein Stück Codefragment gezeigt.
Ich nenne hier keinen Namen, derjenige wird schon bescheidwissen^^.

(Für dieses Beispiel verschärfe ich das Problem noch etwas, damit alle genau den Unsinn sehen, welcher so programmiert werden kann.)

Auf jeden Fall gab es eine Klasse, welche ein Sprite verwaltet hat. Das SpriteObjekt hatte unter anderem eine Funktion zum Anzeigen auf einer Oberfläche, die uns hier nicht weiter interessiert.
Viel interessanter war die Funktion, mit der das Bild des Sprites geändert werden konnte.

Hier der (verschärft überarbeitete) Codeausschnitt der Spriteklasse:
CPP:
class Sprite
{

// Viele Funktionsdefinitionen, die uns nicht interessieren.
// Auch der ctor und der dtor interessiert uns nicht.

public:
   // Die Funktion zum Ändern des Sprites
   void ChangeSprite(std::istream& neuesBild);

private:
   // Noch ein paar Variablen, die wir brauchen.
   Bild* pSprite; // Der Pointer für das Bild
   int geaendert; // Ein Zähler, der für Debug und Auswertungszwecke die Anzahl der Bildänderungen zählt
   Mutex mutex;   // Der Code soll Threadsicher sein, das Syncro-Objekt ist ein Mutex

}

Nicht perfekt, aber auch (noch) nicht schlimm. Eine Funktion, 3 Variablen, davon 1 Pointer.

Die Funktion zum Ändern des Bildes ist die, welche uns in diesem Tutorial interessiert.

Ganz schlechter Code

Die Funktion ChangeSprite():
CPP:
void Sprite::ChangeSprite(std::istream& neuesBild)
{
   lock(&mutex);              // Thread sperren
   delete pSprite;            // Altes Sprite löschen
   ++geaendert;               // Änderungen zählen
   pBild=new Bild(neuesBild); // Neues Bild laden und in Pointer speichern.
   unlock(&mutex);            // Thread wieder freigeben
}

Sieht doch eigendlich ganz gut aus. Der alte Pointer wird freigegeben, ein neues Bild wird geladen und das ganze ist sogar Threadsicher.
Ist aber in Wirklichkeit so schlechter Code, dass er verboten gehört:

Denkt einmal daran, was passiert, wenn der Stream nicht (mehr) offen ist. Oder die Laderoutine aus einem anderem Grund fehlschlägt. Der Grund selbst ist dabei völlig Egal. Oder new: wenn ein OutOfMemory geworfen wird, weil der Speicher für das neue Bild einfach nicht mehr ausreichend war. Möglichkeiten gibt es viele.

Was passiert?
1. Der Thread wird gesperrt, das Mutex-Objekt wird an diesen Thread gebunden.
2. das alte Bild wird gelöscht. (wobei z.B. einfach davon ausgegangen wird, dass das Bild wirklich noch da ist)
3. der Zähler wird erhöht.
4. das neue Bild soll aus dem Stream gelesen werden
4a) KNALL - der Stream ist ja nicht gültig, weil die Datei z.B. nicht mehr offen ist.
5. Aufgrund der Ausnahme wird die Funktion vorzeitig verlassen. Es gibt keine lokalen Variablen also ist nix vom Stack freizugeben.

Die neue Situation.
Das alte Bild ist nicht mehr,
der Geändertzähler ist schon verändert,
Ein neues Bild gibt es noch nicht,
Und obendrein ist der Mutex noch an diesen Thread gebunden. Eine eventuell parallel laufende Zeichenroutine, welche den Thread anfordern würde, würde jetzt ewig warten. Das Programm scheint eingefroren zu sein.

Die Theorie vom Ausnahmesicheren Code.

Zu erst einmal: Ausnahmefreien Code kann es fast nicht geben. Ein auch nur halbwegs komplexes Programm hat immer mit Wiedrigkeiten zu kämpfen, wie nicht gefundene Dateien, zu knappen Speicher, fehlerhaften Benutzereingaben oder einfach einer unterbrochenen Netzwerkverbindung, weil mal wieder gerade der 24-Stunden-DC dazwischengekommen ist (Welcher DSL-Benutzer kennt das nicht - der Disconnect kommt genau im unpassendstem Moment)

Ausnahmesicherer Code ist Code, welcher 3 Verschiedene Garantien geben kann.
1. Die Einfache Garantie,
2. Die Starke Garantie,
3. Die Nichtauslösegarantie.

Die Einfache Garantie

Die einfache Garantie gibt ein Code dann, falls eine Ausnahme auftritt, dass kein Speicherleck entsteht und dass die internen Datenstrukturen in einem gültigen Zustand verbleiben.
Der genaue Zustand ist nicht weiter bekannt.

Im oberen Beispiel würde das bedeuten, dass egal ob eine Ausnahme auftritt der Mutex wieder entsperrt wird, und dass bei verlassen der Funktion ein gültiges Bild im Pointer steht.
Ob es das alte oder das neue Bild ist, ist dabei Egal. Wenn es das alte Bild ist, darf der Zähler aber nicht verändert worden sein. Ist es das neue Bild, muss der Zähler auch um eins erhöt worden sein.
Welche der beiden Varianten letztendlich stattfinden ist egal, das Programm läuft auf jeden Fall sauber weiter.

Die Starke Garantie

Die starke Garantie gibt ein Code genau dann, dass wenn eine Ausnahme auftritt nix verändert wird. Also ein RollBack. Es ist so, als wenn die Funktion nie aufgerufen worden wäre.

Die Nichtauslösegarantie

Die Nichtauslösegarante besagt genau das, was der Name sagt. Innerhalb der Funktion kann nie und unter keinen Umständen eine Ausnahme auftreten.
Eine solche Garantie bieten nur die einfachsten und grundlegensten Operationen. Eine Funktion, die 2 Integers addiert, und das Ergebnis zurückgibt, kann die Nichtauslösegarantie anbieten. (Voraussetzung ist, dass der Integerüberlauf abgeschaltet ist oder 100% sichergestellt ist, dass die Parameter niemals zusammen mehr als den maximalen Wertebereich des Rückgabewertes überschreiten. Diese Garantie ist sehr selten.

Kommen wir nun dazu, den obrigen Code (fast) stark Ausnahmesicher zu machen. Fast, weil wir eine Kleinigkeit nicht verhindern können.

Zuerst das Mutex:

Gekapselte Resourcen

Ein Mutex ist eine Resource, wie z.B. auch Speicher. Wenn ein Thread einen Mutex an sich reisst, sollte er ihn so schnell wie irgend Möglich wieder freigeben.
Indem wir eine ganz kleine Klasse verwenden, um die Mutexverwendung zu kapseln, vermeiden wir das komplette Problem der Wiederfreigabe.

CPP:
class MutexLocker
{

public:
   // ctor
   explicit MutexLocker(Mutex *pMutex)
      : _pMutex(pMutex)
   {
      lock(_pMutex); // Mutex Sperren
   }


   // dtor
   ~MutexLocker()
   {
      unlock(_pMutex); // Mutex freigeben
   }

private:
   Mutex* _pMutex; // Die Variable (der MutexPointer)

}

Das ist alles. Es ist eine Klasse, die ein Mutex Kapselt - Besser gesagt, welche die MutexSperre Kapselt.
Eine Funktion, welche diese Klasse auf dem Stack! anlegt (Instanziiert) Sperrt den Thread und bei verlassen der Funktion wird die Sperre automatisch wieder aufgehoben. Dabei ist Egal, ob die Funktion durch eine Ausnahme oder durch ganz normales beenden verlassen wird.

Die Lösung

Wir können den Code oben damit folgendermassen Ändern:
CPP:
void Sprite::ChangeSprite(std::istream& neuesBild)
{
   MutexLocker(&mutex);       // Thread sperren
   delete pSprite;            // Altes Sprite löschen
   ++geaendert;               // Änderung zählen
   pBild=new Bild(neuesBild); // Neues Bild laden und in Pointer speichern.
} // Die Mutexsperre wird automatisch freigegeben, weil der dtor von ml aufgerufen wird.

Als nächstes kümmern wir uns um das Laden und neu erstellen des neuen Bildes. Wir können es ja erstmal in einem 2. Pointer Speichern.
Wenn das ordnungsgemäss geklappt hat, geben wir den alten Frei und übertragen das neue Sprite auf den Klassenpointer:
CPP:
void Sprite::ChangeSprite(std::istream& neuesBild)
{
   MutexLocker ml(&mutex);          // Thread sperren (ml wird auf dem Stack angelegt)
   Bild* pTemp=new Bild(neuesBild); // Neues Sprite laden.
   delete pSprite;                  // Altes Sprite löschen
   pSprite=pTemp;                   // Texporär geladenes Bild im Klassenpointer Speichern.
   ++geaendert;                     // Änderung zählen
} // Die Mutexsperre wird automatisch freigegeben, weil der dtor von ml aufgerufen wird.

Fertig.
Wenn aus irgend einem Grund das Laden des neuen Sprites fehlschlägt gibt es eine Ausnahme und diese Funktion wird vorzeitig beendet.
Der Speicher ist dann noch nicht reserviert worden und der Änderungszähler ist nicht verändert. Der Klassenpointer enthält noch das Alte Bild.

Aber das ist dann doch schon die Starke Garantie - oder?

Nein. Leider nicht - da der Eingabestream verändert worden sein kann. Das Bild könnte ja schon gelesen worden sein, aber die new-Anforderung ist fehlgeschlagen.
Die LesePosition im Stream kann jetzt anders als vorher sein.
Das Resultat ist also unbestimmt, aber die Klassenvariablen sind alle sauber und der Mutex ist bei Funktionsbeendigung freigegeben.

Um jetzt statt der einfachen Garantie die starke Garantie anbieten zu können, müssten wir den Parameter der Funktion verändern. Wenn wir statt des Streames z.B. einen String übergeben (welcher beispielsweise als Dateiname interpretiert werden könnte und die Laderoutine (der ctor von Bild) mit dem Dateinamen klarkommt, würde die Funktion sogar die Starke Garantie anbieten können.

Fazit

Code kann Ausnahmesicher programmiert werden. Zumindest die einfache Garantie sollte jede Funktion in euren Programmen anbieten. Nach allen potentiellen Fehlerquellen ausschau zu halten ist sicherlich nicht ganz einfach und Anfänger werden sicherlich auch mal wie ganz oben gezeigt programmieren. Aber wenn man ein bisschen Nachdenkt, über dass, was man eigendlich erreichen will, ist es gar nicht so schwer.
Und: Es ist kein Beinbruch, nicht überall die starke Garantie anzubieten, wenn diese sich nur mit erheblichen Mehraufwand erreichen liesse. Die einfache Garantie ist in vielen Fällen ausreichend.

Ich hoffe ein paar Denkanstösse gegeben zu haben.

AFE-GmdG
_________________
CPP:
float o=0.075,h=1.5,T,r,O,l,I;int _,L=80,s=3200;main(){for(;s%L||
(h-=o,T= -2),s;4 -(r=O*O)<(l=I*I)|++ _==L&&write(1,(--s%L?_<(L)?--_
%6:6:7)+\"World! \\n\",1)&&(O=I=l=_=r=0,T+=o /2))O=I*2*O+h,I=l+T-r;}
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden E-Mail senden Website dieses Benutzers besuchen
Kampfhund
Super JLI'ler


Alter: 41
Anmeldedatum: 20.07.2002
Beiträge: 408

Medaillen: Keine

BeitragVerfasst am: 01.03.2009, 15:41    Titel: Antworten mit Zitat

Siehe auch: Resource Aquisition Is Initialization (Design Pattern)
_________________
Kochen ist ein NP-schweres Optimierungsproblem.
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden Website dieses Benutzers besuchen
Beiträge der letzten Zeit anzeigen:   
Neues Thema eröffnen   Neue Antwort erstellen    JLI Spieleprogrammierung Foren-Übersicht -> Tutorials Alle Zeiten sind GMT
Seite 1 von 1

 
Gehe zu:  
Du kannst keine Beiträge in dieses Forum schreiben.
Du kannst auf Beiträge in diesem Forum nicht antworten.
Du kannst deine Beiträge in diesem Forum nicht bearbeiten.
Du kannst deine Beiträge in diesem Forum nicht löschen.
Du kannst an Umfragen in diesem Forum nicht mitmachen.


Powered by phpBB © 2001, 2005 phpBB Group
Deutsche Übersetzung von phpBB.de

Impressum