C++11, TR1, Lambda-Ausdrücke und Parallelisierung

Alles zu Zusi-Performance, Frameraten, ruckelnden Bildern, Grafik, Treibern usw.
Antworten
Nachricht
Autor
Benutzeravatar
Roland Ziegler
Beiträge: 5508
Registriert: 04.11.2001 22:09:26
Wohnort: 32U 0294406 5629020
Kontaktdaten:

C++11, TR1, Lambda-Ausdrücke und Parallelisierung

#1 Beitrag von Roland Ziegler »

In der Vergangenheit hatte ich mehrfach auf die lange bestehende Fähigkeit von C++ hingewiesen, jede Menge Funktionalität typsicher benutzen zu können, ohne dass den Bibliotheken, die diese Funktionalität bereitstellen, der Typ vorher bekannt ist. In C++ heißt der Mechanismus dazu "Templates" und die bekannteste Bilbilothek, die diese Templates intensivst nutzt, ist die STL, die "Standard Template Library", auch schlicht als "C++ Standard Library" bekannt. Die bekanntesten Klassen dieser Bibliothek sind die "Container", Aggregate für Daten mit den verschiedensten Strukturen.

Das Konzept war so überzeugend, dass es später von C# und auch Java übernommen wurde. Dort heißen die "Templates" "Generics" und die "Container" "Collections". (Bestimmte technische Unterschiede seien für diese Betrachtung ausgeklammert.) In C# ging es dann weiter. Dort gab es die nette Einrichtung der Delegaten, die, wie sich herausstellte, mehr, bzw. anderes Potenzial enthielten als die Funktor-Klassen der STL. In C# 3 wurden die Delegaten zu Lambda-Ausdrücken weiterentwickelt. Der Lambda-Ausdruck ist eine Art anonyme Funktionsdeklaration und von seinem Wesen her nicht einmal objektorientiert, aber ausgesprochen mächtig.

Nun schauten die C++-Leute auf C# und brachten den Lambda-Ausdruck zu C++ zurück. Er gehört zu den neuen Eigenschaften von C++11, das im August genormt wurde. C++11 enthält noch viel mehr und wohl kaum ein Compiler beherrscht schon alle Feinheiten, aber manches kann bereits genutzt werden.

Dazu gehört die Erweiterung der STL mit dem Paket TR1 (ganz unscheinbar für "Technical Report 1"), das u.a for-each-Schleifen enthält, die sich, in Kombination mit Lambda-Ausdrücken, genauso verhalten wie in C#.

Code: Alles auswählen

list<MyClass> & mylist = getFromSomewhere();
int local_var = 0;
for_each (mylist.begin(), mylist.end(), [&] (MyClass & obj) { 
  obj.someFunction (var);
});
Der dritte Parameter ist der Lambda-Ausdruck mit den anonymen Funktion. Eine wesentliche Eigenschaft ist die Fähigkeit, aus der aufrufenden Klasse Variablen mitzunutzen, lokal oder Member. Man nennt dies Capturing. Mit dieser Fähigkeit kann man normalerweise bisherige for-Schleifen auf einfache Art in for_each-Funktionsaufrufe wandeln.

Soweit. so gut.

Der besondere Reiz kommt jetzt, wenn man eine solche Schleife auch noch parallelisieren kann, um die heutigen Mehrkernprozessoren effizient zu nutzen.

Auch wenn es zunächst Microsoft-spezifisch wird, das muss nicht so bleiben. MS lieferte mit Visual Studio 2010 eine neue Bibliothek zur Parallelisierung mit, die auf TR1 aufsetzt. Diese ist, wie vieles, was MS für Entwickler heutzutage bereitstellt, recht wohl durchdacht, d.h. sie bringt die nötige Abstraktion und die Hierarchie mit, die auch die .Net-Bibliotheken auszeichnen. (Während MS beim Endbenutzer von zunehmender Dummheit auszugehen scheint, sieht man den Entwickler offensichtlich als immer anspruchsvolleren Kunden, dem man gerecht werden will.)

Code: Alles auswählen

list<MyClass> & mylist = getFromSomewhere();
int local_var = 0;
parallel_for_each (mylist.begin(), mylist.end(), [&] (MyClass & obj) { 
  obj.someComputationIntensiveFunction (var);
});
Dies ist das Sahnehäubchen. Parallelisierung mit minimalem Aufwand. Aber genauso ist es nutzbar. Dazu muss intern natürlich einiges gerechnet werden. Dies spielt sich in den Klassen dieser Bibliothek ab, die zugänglich sind und auch einzeln genutzt werden können, wenn z.B. die Parallelisierung nicht so einfach zu gestalten ist wie hier.

U.a. wird ermittelt, wieviele Kerne der Prozessor hat. Dann wird die Schleife in mehrere Abschnitte aufgeteilt und jeder Abschnitt in einem eigenen Thread abgearbeitet. Im Prinzip alles keine Hexerei und die logische Kombination und Fortentwicklung diverser bekannter Ansätze, aber meilenweit entfernt von den allerersten Versuchen in den späten 80ern, damals für Fortran.

Läuft übrigens ab XP SP3.

Ich werde für meine Werkzeuge, die ja durchaus rechenintensiv sind, versuchen, diverse Schleifen auf parallel zu ändern, sofern die Schleifen dafür geeignet sind - keine Abhängigkeiten der Iterationsschritte voneinander - und der Aufwand im jeweiligen Kontext sich in Grenzen hält. Erste Versuche beim Erstellen eines Pseudo-DEMs waren ganz vielversprechend. Es könnte allerdings kommen, das zukünftig auch ein Prozessor mit mehr als zwei Kernen bei TransDEM oder dem Geländeformer heiß wird. ;D

Benutzeravatar
Michael_Poschmann
Beiträge: 19880
Registriert: 05.11.2001 15:11:18
Aktuelle Projekte: Modul Menden (Sauerland)
Wohnort: Str.Km "1,6" der Oberen Ruhrtalbahn (DB-Str. 2550)

Re: C++11, TR1, Lambda-Ausdrücke und Parallelisierung

#2 Beitrag von Michael_Poschmann »

Roland Ziegler hat geschrieben:Es könnte allerdings kommen, das zukünftig auch ein Prozessor mit mehr als zwei Kernen bei TransDEM oder dem Geländeformer heiß wird. ;D
... so lange die Stellwerks-Entwicklung nicht darunter leidet... 8)

Grüße, sich jahresendzeitbedingt eher "Apfel, Nuss und Mandelkernen" zuwendend

Michael

Benutzeravatar
Roland Ziegler
Beiträge: 5508
Registriert: 04.11.2001 22:09:26
Wohnort: 32U 0294406 5629020
Kontaktdaten:

Re: C++11, TR1, Lambda-Ausdrücke und Parallelisierung

#3 Beitrag von Roland Ziegler »

Michael_Poschmann hat geschrieben: ... so lange die Stellwerks-Entwicklung nicht darunter leidet...
Jener steht nach der gewünschten GF-Anpassung und der dringend notwendigen Urlaubsreise nichts mehr im Wege. ;)

Die Umstellung auf neuen Entwicklungsrechner ist erfolgt. Der GF bleibt aber zäh zum Debuggen, und auch da tut dann Beschleunigung gut. Leider lässt sich Parallelisierung nicht auf das eigene Gehirn übertragen.

Benutzeravatar
Carsten Hölscher
Administrator
Beiträge: 33442
Registriert: 04.07.2002 00:14:42
Wohnort: Braunschweig
Kontaktdaten:

Re: C++11, TR1, Lambda-Ausdrücke und Parallelisierung

#4 Beitrag von Carsten Hölscher »

Aber man muß selbst entscheiden, ob die Schleife parallelisierbar ist und ggf. nötige Synchronisation auch manuelll selbst vornehmen, oder?
Aber trotzdem eine feine Sache, wobei das ganze ja auch Overhed mit sich bringt,auch wenn man ihn nicht sieht. Der Einsatz der Funktion muss also sicherlich mit Bedacht gewählt werden.

Carsten

Mr. X
Beiträge: 1337
Registriert: 04.05.2008 22:12:22
Kontaktdaten:

Re: C++11, TR1, Lambda-Ausdrücke und Parallelisierung

#5 Beitrag von Mr. X »

Danke für diesen gut geschriebenen und interessanten Beitrag. Ich hab zwar schon einiges über C++11 gelesen, und einige Funktionen verwendet (nullptr, std::function/std::bind, std::to_string, ...), aber eines hab ich jetzt erst bemerkt:
Der dritte Parameter ist der Lambda-Ausdruck mit den anonymen Funktion. Eine wesentliche Eigenschaft ist die Fähigkeit, aus der aufrufenden Klasse Variablen mitzunutzen, lokal oder Member. Man nennt dies Capturing. Mit dieser Fähigkeit kann man normalerweise bisherige for-Schleifen auf einfache Art in for_each-Funktionsaufrufe wandeln.
Das Capturing hört sich interessant an, diese Funktionalität war mir bisher unbekannt. Da wird std::for_each ja plötzlich benutzbar. (for_each gabs ja schon vor dem TR1, afaik, aber eben keine Lambdas. Lambdas hatte ich bislang für eine Kurzschreibweise einer Funktion gehalten, aber offenbar habe ich sie unterschätzt.)
Während MS beim Endbenutzer von zunehmender Dummheit auszugehen scheint, sieht man den Entwickler offensichtlich als immer anspruchsvolleren Kunden, dem man gerecht werden will.
In Anbetracht des Schneckentempos bei der Weiterentwicklung deren Compilers hinsichtlich C++11 (und C99, dass sie garnicht unterstützen), halte ich das für fraglich ;). Auf Variadic Templates, m.E. eine der interessasten Neuerungen, dürfen wir wohl noch ewig warten. vNext (nächste Version von MSVC) wirds nicht enthalten.
Zuletzt geändert von Mr. X am 23.12.2011 17:32:10, insgesamt 1-mal geändert.

Benutzeravatar
Roland Ziegler
Beiträge: 5508
Registriert: 04.11.2001 22:09:26
Wohnort: 32U 0294406 5629020
Kontaktdaten:

Re: C++11, TR1, Lambda-Ausdrücke und Parallelisierung

#6 Beitrag von Roland Ziegler »

Carsten Hölscher hat geschrieben:Aber man muß selbst entscheiden, ob die Schleife parallelisierbar ist und ggf. nötige Synchronisation auch manuelll selbst vornehmen, oder?
Aber trotzdem eine feine Sache, wobei das ganze ja auch Overhed mit sich bringt,auch wenn man ihn nicht sieht. Der Einsatz der Funktion muss also sicherlich mit Bedacht gewählt werden.
Sicher muss man mit Bedacht vorgehen und sich jede Schleife genau angucken. Wird auch schwieriger zu debuggen.

Interessant die neuen Synchronisations-Konstrukte, die es ganz ähnlich auch in .Net 4 gibt. Bei denen wird nicht unbedingt der Thread sofort unterbrochen, wenn er blockieren würde, sondern noch ein paar mal probiert. Dies soll im Endeffekt effizienter sein als jedesmal ein Taskwechsel. (Außerhalb paralleler Schleifen soll man die aber nicht nutzen.)

Aber auch bei deren Verwendung muss man Zugriff auf gemeinsame Ressourcen sorgfältig platzieren, sonst macht man sich die Geschwindigkeitsvorteile der Parallelisierung schnell zunichte.

Insgesamt kommt hier ja nichts grundsätzlich neues, und alle Risiken mehrläufiger Prozesse gelten unverändert weiter.
Mr. X hat geschrieben:Das Capturing hört sich interessant an, diese Funktionalität war mir bisher unbekannt. Da wird std::for_each ja plötzlich benutzbar. (for_each gabs ja schon vor dem TR1, afaik, aber eben keine Lambdas. Lambdas hatte ich bislang für eine Kurzschreibweise einer Funktion gehalten, aber offenbar habe ich sie unterschätzt.)
Die Mächtigkeit der Lambda-Ausdrücke ist auch mir erst aufgegangen, als ich in sie einem C#-Buch ausführlich behandelt sah ("C# in Depth").

Benutzeravatar
Roland Ziegler
Beiträge: 5508
Registriert: 04.11.2001 22:09:26
Wohnort: 32U 0294406 5629020
Kontaktdaten:

Re: C++11, TR1, Lambda-Ausdrücke und Parallelisierung

#7 Beitrag von Roland Ziegler »

Mittlerweile habe ich sowohl im Geländeformer als auch in TransDEM ein wenig von der neuen C++-Funktionalität eingebaut, im Zuge von Überarbeitung und Erweiterung um neue Funktionalität.

Auch die Microsoft-spezifische Parallelverarbeitungsbibliothek kommt zum Einsatz.

Man muss immer wieder betonen, welch einen langen Weg Microsoft in den letzten 15 Jahren bei C++ zurückgelegt hat. Zu Zeiten von MFC, ATL oder COM wäre es für MS schlicht undenkbar gewesen, auf Standardbibliotheken aufzubauen. Das ist heute ganz anders. Nicht nur spielt Microsoft eine aktive Rolle bei der Weiterentwicklung des C++-Standards, sondern liefert auch ohne Verzug Implementierungen für die Anwender.

Sicher, die Parallelverarbeitung ist noch proprietär. Aber sie setzt direkt und nahtlos auf C++11 bzw. TR1 auf und senkt so massiv die Hemmschwelle für die Nutzung.

Das Beispiel, das ich hier vorstellen möchte, stammt aus der Umsetzung einer neuen Georeferenzierungsvariante in TransDEM, die Anwendung unregelmäßiger Dreiecksnetze. Hierbei wird die zu georeferenzierende Rasterkarte mit einer beliebigen Zahl von Stützpunkten überzogen. Für jeden Stützpunkt werden die gewünschten Koordinaten im Zielsystem vorgegeben. Aus den Stützpunkten entsteht durch Triangulation nach Delaunay/Bourke ein Dreiecksnetz, im Außenbereich extrapoliert nach Ziegler 8).

Die drei Stützpunkte für jedes Dreieck liefern die Parameter für eine affine Transformation aus Translation, Skalierung, Rotation und Scherung.

Hier die Ausgangslage, die Karte ist etwa 12 MPIxel groß.
Bild
Zugewiesen wurden die vier Eckpunkte im Koordinatengitter sowie ein fünfter, unten in der Mitte, um zu prüfen, was mit quasi flächenlosen Dreiecken passiert.

Die eigentliche Konvertierung in das Zielkoordinatensystem, hier UTM, ist ziemlich rechenaufwändig. Meine Lösung besteht aus zwei getrennten Phasen mit je einer eigenen Schleife, die von vorneherein so konzipiert wurde, dass sie sich parallelisieren lässt.

Die erste Schleife läuft über jeden Punkt der Original-Bitmap und ermittelt, in welchem Dreieck er liegt. Dann wird der Punkt in das Zielkoordinatensystem konvertiert, mit Hilfe der affinen Transformation aus dem gefunden Dreieck. Den Punkt im Zielsystem wird man nicht genau treffen. Stattdessen werden die vier Nachbarn gesucht, und dort überall das Dreieck hinterlegt, zusammen mit einem bilinearem Wichtungsfaktor. Der Aufwand ist nötig, da in Nähe der Kanten und Stützpunkte auch mehr als ein Dreieck zur Transformation beitragen kann.

In der zweiten Phase wird über sämtliche Punkte der Ziel-Bitmap (UTM) iteriert. Je Punkt werden dann der Reihe nach die zuvor dort hinterlegten Transformationsdreiecke mit ihrer individuellen Wichtung angewandt, um die Farbwerte des Pixels zu ermitteln. Die Farbbestimmung kann bilinear oder bikubisch erfolgen.

In der ersten Phase wird der Speicherbedarf stetig ansteigen, da die gefundenen Dreiecke für die zweite Phase zwischengespeichert werden müssen. In der zweiten Phase mit der Farbtransformation bleibt der Speicherbedarf konstant.

Selbst mit dem relativ geringen Overhead eines STL-Containers ist der Speicherbedarf für die Dreiecksreferenzen erheblich. Da für große Bitmaps die Gefahr des Speicherüberlaufs bei 2GB zu befürchten war, habe ich den Algorithmus über einen beschränkten Zwischenpuffer laufen lassen, der in der Voreinstellung nicht über etwa 500MB wächst. (Bildverarbeitung kann sehr speicherintensiv werden.) Beide Phasen werden somit bei größeren Bitmaps mehrfach durchlaufen.

Die große Ernüchterung kam, als ich die Schleifen der beiden Phasen nach anfänglicher serieller Verarbeitung und den dort einfacheren Funktionstests auf parallel umstellte. Denn es trat so gut wie kein Rechenzeitgewinn auf. Aus ursprünglich knapp 3 Minuten wurden nur 2:43 min.

Das war zunächst also sehr enttäuschend.

Grafisch sieht das so aus: Das obere Bild zeigt die von TransDEM genutzten Ressourcen während der Konvertierung, das untere als Snapshot die Leitung der einzelnen CPU-Kerne.

Bild
Bild

Im oberen Bild sieht man im mittleren Diagramm deutlich den Speicherverbrauch. Durch den begrenzten Zwischenpuffer wird Phase 1 und Phase 2 insgesamt dreimal aufgerufen. In der ersten Phase steigt der Speicherbedarf jeweils an, um in der zweiten, sehr viel kürzeren Phase konstant zu bleiben.

Im unteren Bild fällt auf, dass kein CPU-Kern wirklich ausgelastet ist, was deutlich nicht im Sinne des Erfinders war.

Ausbremsend wirkt immer die Verriegelung gemeinsamer Speicherstrukturen. Man sollte solche Zugriffe in parallelen Schleifen zwar möglichst vermeiden, aber in der Praxis ist das oft genug nicht möglich. Ich war mir recht sicher, diese leider notwendigen Zugriffe korrekt gesichert zu haben, über Spinlocks, nicht über Win32-Critcal Sections („Mutex“ außerhalb der Windows-Welt).

Spinlocks führen bei gescheitertem Zugriffsversuch nicht zum Taskwechsel, der recht teuer ist, sondern nur zu Wiederholung des Zugriffsversuchs. Der Thread läuft weiter, wenn auch unnütz, aber das soll weit günstiger sein als Thread-Unterbrechung.

Nun, nach längerem Suchen fand ich dann die Ursache des Problems: In einer der benutzten Klassen wurde tatsächlich noch noch ein klassischer Mutex verwendet. Den habe ich dann über einen Schalter in einen Spinlock verwandelt und danach sah das Ergebnis deutlich anders aus.

Aus 2:43 min oder 163 sec wurden 40 sec, Faktor 4. Das war es, was ich erhofft hatte.

Hier dazu die Grafiken, mit identischem Zeitmaßstab wie zuvor. Und nun sieht man, dass alle Kerne richtig ins Schwitzen kommen, wie gewünscht. Einiges an Leerlauf ist sicher auch dabei, wegen der Mehrfachversuche durch Spinlocks. Aber der Nutzen überwiegt die Kosten bei weitem, wie mit der kurzen Gesamtzeit eindrucksvoll belegt.

Bild
Bild

Das Endergebnis bei diesem Beispiel ist übrigens absolut unspektakulär. Es sei hier nur der Vollständigkeit halber angeführt:
Bild
Bei genauerem Hinsehen würde man feststellen, dass die violetten Gitterlinien aus TransDEM eine bessere Deckung mit den aufgedruckten schwarzen Gitterlinien aufweisen, als dies bei der klassischen Georeferenzierung mit nur einem Transformationsdreieck der Fall wäre, einfach deswegen, weil es sich hier um eine gescannte Papierkarte handelt, wo häufig leichte Verzerrungsfehler auftreten.
Zuletzt geändert von Roland Ziegler am 30.06.2012 08:29:30, insgesamt 1-mal geändert.

Benutzeravatar
Michael_Poschmann
Beiträge: 19880
Registriert: 05.11.2001 15:11:18
Aktuelle Projekte: Modul Menden (Sauerland)
Wohnort: Str.Km "1,6" der Oberen Ruhrtalbahn (DB-Str. 2550)

Re: C++11, TR1, Lambda-Ausdrücke und Parallelisierung

#8 Beitrag von Michael_Poschmann »

Hallo Roland,

nette Fingerübung, Respekt. Hoffentlich bleibt noch ein wenig Rechenzeit auf einem der Kerne fürs Stellwerk übrig. :ausheck Ansonsten bin ich natürlich gespannt, inwieweit IVLZ-Unterlagen von dieser Technologie profitieren können.

Gruß
Michael

Benutzeravatar
Hans-Peter Schramm
Beiträge: 1181
Registriert: 11.11.2001 14:15:59
Aktuelle Projekte: Bottwartalbahn Marbach-Heilbronn (DB-Schmalspur)
Wohnort: Elmshorn

Re: C++11, TR1, Lambda-Ausdrücke und Parallelisierung

#9 Beitrag von Hans-Peter Schramm »

Hallo Roland,
Respekt vor diesen großartigen Dingen. Da kommen Erinnerungen hoch, an den Beginn meiner beruflichen Laufbahn, als ich in ganz anderem Zusammenhang mit Abbildungen bzw. Transformationen zu tun hatte, an die ich jetzt wieder erinnert werde.
Es ging um eine näherungsweise Abwicklung von gekrümmten Flächen in die Ebene.

Das waren noch Zeiten, als man sich wirklich mit Inhalten beschäftigen durfte, und nicht jede Entwicklung von Controllern zeredet wurde und letztendlich alles im firmenpolitischen Hickhack untergeht.

Gruß
Hans-Peter

Benutzeravatar
Roland Ziegler
Beiträge: 5508
Registriert: 04.11.2001 22:09:26
Wohnort: 32U 0294406 5629020
Kontaktdaten:

Re: RAII, Resource Acquisition is Initialization

#10 Beitrag von Roland Ziegler »

Hans-Peter Schramm hat geschrieben:... als ich in ganz anderem Zusammenhang mit Abbildungen bzw. Transformationen zu tun hatte,
Michael_Poschmann hat geschrieben:... inwieweit IVLZ-Unterlagen von dieser Technologie profitieren können.
Affine Abblidungen/Transformationen sind so exotisch nicht. In ihrer einfachen Form, nur Translation und Rotation, sind sie Basis jeder 3D-Szene, so auch in Zusi, mit dem vermutlich einzigen Unterschied, dass man sie dort nicht wahrnimmt.

Ob sich der Ansatz unregelmäßiger Dreiecksnetze für verzerrte Gleispläne wirklich praktikabel nutzen lässt, kann ich noch nicht sagen und will hier auch nichts versprechen. Der Algorithmus selbst funktioniert, und wird für Karten und Pläne wie im gezeigten Beispiel schon deutliche Verbesserungen bringen. Die Gleispläne bergen eine zusätzliche Herausforderung: Die Aufstellung des Dreiecksnetzes selbst. Wie es formal aussehen sollte, ist einfach vorzustellen. Aber es dann auch zu bemaßen steht auf einem anderen Blatt. Ich werde noch ein wenig experimentieren.

Zurück zu C++.

Diesen Beitrag möchte ich einer Softwaretechnik widmen, die sich RAII abkürzt, Resource Acquisition is Initialization, leider völlig unpassend, um aus dem Namen den Nutzen abzuleiten.

Als sich das OO-Paradigma vor etwa 15 Jahren weitgehend etabliert hatte, und die informationstechnischen Vordenker mit ihren Thesen den Weg zu aufgeschlossenen Entwicklern fanden - eine mittlere Sensation, waren doch vorher Informatik und Programmierung zwei verschiedene Welten - verbreiteten sich auch kleinere Techniken und Verfahren, die zu besserem Softwaredesign führten. Ein Stichwort sind Entwurfsmuster, Design Patterns, Klassenstrukturen, die bestimmte Teilaufgaben auf immer die gleiche Art lösen, ohne unbedingt als Klassenbibliothek ausgeprägt sein zu müssen. Das Singleton- oder Factory-Pattern beispielsweise wird jeder kennen, der in der OO-Welt zu Hause ist. Warum RAII nie den Titel Pattern bekommen hat, entzieht sich meiner Kenntnis, es ist auch nicht komplexer als Singleton.

Die Idee von RAII ist simpel. Jede Ressource, die in einem Programm benötigt wird, zum Beispiel der Zugriff auf eine Datei, muss auch wieder freigegeben werden. Bei RAII definiert man die Freigabe bereits in dem Moment, in dem man die Ressource akquiriert, mit Hilfe eines Verwaltungsobjektes. Wird das Verwaltungsobjekt angelegt, wird die Ressource akquiriert, wird das Verwaltungsobjekt zerstört, wird die Ressource freigegeben.

Das Prinzip dabei ist Konstruktor und Destruktor. OO-Sprachen, in denen die Klassen dedizierte Destruktoren haben, wie C++, tun sich besonders leicht. Die Verwaltungsobjekte werden als lokale Variablen einer Klassenmethode auf dem Stack angelegt. Wird die Methode verlassen, völlig unabhängig davon wann und wie, wird der Stack bekanntlich aufgeräumt und das Verwaltungsobjekt dabei zerstört. Hierbei wird dessen Destruktor aufgerufen, und der gibt die Ressource, immer, in allen denkbaren Fällen einschließlich Exception, absolut zuverlässig.

OO-Anfänger tun sich manchmal schwer, wenn sie den Sinn und die Möglichkeiten von Konstruktoren und Destruktoren erfassen sollen. RAII ist ein hervorragendes Beispiel für Klassen, die überhaupt nur aus Konstruktor und Destruktor bestehen.

Garbage-Collection-Sprachen wie C# oder Java verfügen nicht über dedizierte Destruktoren. Hier muss man sich behelfen und RAII in einen try-finally-Block packen. Das ist natürlich fehlerträchtig. In C# stand von Anfang an das Schlüsselwort using zur Kapselung eines RAII try-finally zur Verfügung, für Java wurde es mit Version 7 im letzten Jahr endlich nachgeholt.

Die weiteren Betrachtungen bleiben bei C++ und sollen konkrete Beispiele vorstellen.

Die Beispiele stehen in direktem Zusammenhang mit meinem vorigen Beitrag. Die dort vorgestellten parallelen Schleifen können auf verschiedene Art verlassen werden. Im Optimalfall durch vollständige Abarbeitung der Schleifen und normale Rückkehr zu höherer Ebene. Ein zweite Möglichkeit ist der vorzeitige Abbruch durch den Benutzer. Threads werden immer von innen gestoppt. Der Abbruchswunsch muss dem Thread mitgeteilt werden. Das geschieht hier über ein einfache boolsche Variable, deren Zugriffsoperationen als atomar gelten, also nicht mehr Mutex udgl verriegelt werden müssen. Zum Beenden der vielen parallelen Threads reicht leider kein vorzeitiges return, denn dieses würde nur einen Thread terminieren. Vielmehr muss eine Exception geworfen werden, die von einer der Verwaltungsklassen der Parallelschleifen bibliotheksintern abgefangen wird, und sämtliche anderen Threads ebenfalls von innen beendet. Und wiederum Exceptions, diesmal unvorhergesehene, sind auch die dritte Möglichkeit des Abbruchs. TransDEM bewegt sich gerne immer wieder in der Nähe der allozierbaren Speicherobergrenzen, manche Benutzer sind da gnadenlos. Man muss also damit rechnen, dass es irgendwann nicht mehr reicht.

In allen Fällen von Schleifenende soll dafür gesorgt werden, dass unabhängig von der Art der Rückkehr, ob über normalen Ablauf, oder über Exception, alle lokalen Variablen oder temporär geänderte Zustände ordentlich wieder aufgeräumt werden, ein typisches Anwendungsfeld für RAII.

Das erste Codebeispiel bezieht sich auf die Klasse, die mir bei der Parallelisierung (letzter Beitrag) zunächst Ärger gemacht hatte, weil sie die klassische CriticalSection benutzt hatte, hier aber dringend ein Spinlock angeraten war.

Ich habe daher zunächst jene Klasse namens GridConversion um den Spinlock intern erweitert, und eine Funktion eingefügt, mit der man hin und her schalten kann. Wichtig ist jetzt, und damit kommt RAII ins Spiel, dass einmal auf Spinlock umgeschaltet, nach Verlassen der Parallelschleife auch sicher wieder zurückgeschaltet wird. Dazu dient die folgende RAII-Klasse, die ich üblicherweise mit Postfix "Guard" versehe.

Code: Alles auswählen

class GridConversionSpinlockEnableGuard {
public:
  // ctor
  GridConversionSpinlockEnableGuard (GridConversion & gridconv) :
	  m_gridconv (gridconv)
	{
	  m_gridconv.useSpinlock (true);
	}
	
	// dtor
  ~GridConversionSpinlockEnableGuard (){
	  m_gridconv.useSpinlock (false);
	}

private:
  GridConversion & m_gridconv;
};
Die RAII-Klasse hat, wie vorher eingeführt, nur Konstruktor und Destruktor. Die Konstruktor wird das Objekt von GridConversion mitgegeben, auf welches sich die Umschaltung bezieht. Der Konstruktor schaltet im referenzierten Objekt die Spinlock-Nutzung ein, der Destruktor wieder aus.

Angewandt wird das per Einzeiler:

Code: Alles auswählen

void myFunction () {

	// ...
	
	// here comes the RAII object
	GridConversionSpinlockEnableGuard grdConvSpinlockGuard (m_gridConv);

	// parallel loop where spinlock is required
	parallel_for (0, max, [&](unsigned loopIdx) {
		// ...
		// Lambda expression with all the code of the parallel loop
		// using m_gridConv functions, now secured by spinlock
		// ...
		
	});

}
Wird myFunction () verlassen, auf welche Art auch immer, so wird das Guard-Objekt zerstört und m_gridConv schaltet zurück auf CriticalSection.


Zweites Beispiel. Hier wird eine RAII-Klasse aus der Microsoft-Parallel-Bibliothek benutzt "Concurrency::scoped_lock", um einen Zugriff per Spinlock "Concurrency::critical_section" zu verriegeln. Man beachte die Namensgebung dieser Microsoft-Klassen, Kleinschreibung und mit Unterstrich, sehr untypisch für MS aber sich nahtlos integrierend in die C++-Standardbibliothek.

Code: Alles auswählen

void someFunction () {

	// ...
	
	// a critical section as a spinlock object 
  Concurrency::critical_section mtx;

	// parallel loop where spinlock is required
	parallel_for (0, max, [&](unsigned loopIdx) {
		// ...
		// Lambda expression with all the code of the parallel loop
		// ...
		
		// a spinlock guard
		Concurrency::critical_section::scoped_lock gd (mtx);
		triWghtArr[m].push_back (make_pair (pTri, wght));
	});
Der zu sichernde Zugriff auf triWghtArr erfolgt ganz zum Schluss des Lambda-Ausdrucks, somit wird unmittelbar der Zuweisung das Guard-Objekt wieder zerstört und der Spinlock freigegeben.

Ein RAII-Variante für Pointer, Heap statt Stack, folgt im nächsten Teil.
Zuletzt geändert von Roland Ziegler am 03.07.2012 22:15:30, insgesamt 1-mal geändert.

Antworten