AFE-GmdG JLI MVP
Alter: 45 Anmeldedatum: 19.07.2002 Beiträge: 1374 Wohnort: Irgendwo im Universum... Medaillen: Keine
|
Verfasst am: 28.02.2009, 23:25 Titel: Ausnahmesicherer Code |
|
|
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;} |
|
|