Inhalt
|
Hintergrund
Vier Kerne, aber ...
Mein kleiner Sony-Rechner mit seinem 13-Zoll-Bildschirm zusammen mit dem 14 Jahre alten 22-Zoll-Röhrenmonitor sind eine feine Sache. Die CPU hat vier Rechenkerne - wenn aber ein Programm unter Volldampf werkelt, zeigt der Windows-Prozeßmanager oft nur eine CPU-Auslastung von 25% an. Drei Kerne tun (fast) nichts ...
Und die Herren Burns und Wellings stellen in ihrem Buch (Kapitel 11.10) concurrent execution abstractions ... for creating concurrent execution engines vor. Richtig, ein Gerüst, eine Rahmenkonstruktion muss her, um eine geeignete Aufgabe mit leichter Hand auf mehrere Kerne verteilen zu können.
Das Rad soll ja im Projekt nicht immer wieder neu erfunden werden - auch ich will es nicht neu (und schlechter) erfinden. Ich habe den Ada-Code also denn doch und wohl oder übel (1) eifrig abgetippt, mir eine banale Anwendung aus der Bildverarbeitung ausgedacht - und das Ganze auch anständig zum Laufen gebracht ...
(1) Den Quellcode gab es einmal auf den Webseiten der Autoren und der war leider unvollständig und fast unbrauchbar.
Konzepte
Typerweiterungen und synchronisierte Schnittstellen
Die prächtige Programmiersprache Ada kennt seit Ada'95 Typerweiterungen mittels tagged types und seit Ada'05 solche Schnittstellen wie sie auch Java anbietet und darüber hinausgehend die synchronisierten Schnittstellen.
Diese Schnittstellen a'la Java erlauben eine gezähmte Form der mehrfachen Vererbung: Ein abgeleitevter Typ kann also Eigenschaften und Funktionen seines Obertypen erben, man kann aber zusätzlich beschreiben, dass Objekte des Typs zum Beispiel auch druckbar oder ausführbar sein sollen. Dazu müssen die Objekte entsprechende rein funktionale Schnittstellen implementiert haben.
Hier soll nun ein Objekt etwas verrichten, etwas tun - dazu muss das Objekt aufgerufen werden können, es muss aufrufbar sein! Klar, das Objekt ist eben eine Routine und muss die folgende Schnittstelle erfüllen:
Callables
Die abstrakte Schnittstelle für aufrufbare Dinge
Die Zeile 10 definiert den Typen Callable_Type als Schnittstelle. Ein Objekt C dieses Typs muss aufgerufen werden können (Zeilen 13, 14) und zwar mit einem (komplexen) Eingangsparameter P und einem Ergebnis vom Typ Result_Type. Eingangs- und Ergebnisparameter werden als generische Typen (Zeilen 5, 6) bereitgestellt.
Der Aufruf des Objektes liefert ein Ergebnis und das braucht ein Plätzchen, wo es abgelegt und später auch abgeholt werden kann, man braucht einen Platzhalter, ein future. Solche futures finden sich etwa auch in der funktionalen Programmiersprache Clojure.
Futures
Die Schnittstellendefinition für solch einen zukünftigen Platzhalter ist:
Das Datum, das Platz finden soll, wird als generischer Typ (Zeile 4) übergeben, mittels Set (Zeile 23) und Get (Zeile 20) kann das Datum abgelegt und abgeholt werden.
Die Schnittstelle ist synchronized (Zeilen 8, 17), das bedeutet, dass sie mittels Tasks oder über geschützte Typen (protected types) implementiert wervden muss und and Future_Type (Zeile 17) fordert die Implementierung der entsprechenden Schnittstelle.
Die abstrakte Schnittstelle für einen Platzhalter
Executors
Der Rezeptur fehlt noch ein Wirkungsträger, die aufrufbare Routine muss ja noch ausgeführt werden, wir brauchen noch ein Ding mit der Eigenschaft, ausführbar zu sein - und dahinter verstecken sich natürlich Tasks, die auf die verschiedevnen CPU-Kerne verteilt werden sollen! Grandios ...
Die abstrakte Schnittstelle für einen Ausführenden
Ein Executor muss natürlich kennen, was er zur Ausführung bringen soll - genau das erfährt er über die generische Schnittstelle, darunter die beiden Ausprägungen zweier generischer Pakete.
In der weiteren Implementierung wird dann ein Pool von Tasks bereitgestellt, um die anstehende Arbeit erledigen zu können.
Genug davon!
Das mag als Anregung reichen - mehr finden Sie nur im Quellcode oder im Buche.
Anwendung
Farbbefüllung von Bildern
Der kleine Rechner ist schnell, ein ziemlich großes Bild im Bitmap-Format wollte ich nach dem Zufallsprinzip mit Farben befüllen. Für die Bearbeitung wird das Bild in Segmente eingeteilt, für jedes Segment ist eine eigene Task zuständig und die Tasks werden auf die CPU-Kerne verteilt ...
Test
Aufgabe erfüllt!
Das Testprogramm ist schön kurz und knapp. 16 Segmente, jedes 4096 x 4096 Pixel groß, waren durch 8 Tasks auf 4 Kernen zu befüllen.
Kurz und knapp
Und alle vier Kerne rackern, 100% CPU-Auslastung, man sieht es auch an den grünen Kurven.
Der Prozeßmanager zeigt es!
Ein Bild
Keine fraktalen Farbverläufe
Am Ende war ich doch neugierig, wie so ein Bild aussieht, bei dem die 24-Bit-Farben für jeden Farbkanal (pseudo-)zufällig erzeugt werden. Ich hatte - ohne nachgedacht zu haben - ein graues Bild erwartet.
Hier das eigens erzeugte 800x800-Pixel-Bild verkleinert auf 300x300 Pixel. Man erkennt großflächige Schlierenmuster.
Ungrau in Zufallsfarben
Gimp spuckt im Histogramm-Dialog einige Zahlen zu den Farbkanälen aus, unter anderem die Mittelwerte; diese sind für Rot 127,6, für Grün 127,5 und für Blau 127,4.
Re-initialisiert man den Zufallszahlgenerator (mit einer zeitabhängigen 'Prise'), so erhält man ähnliche Schlieren, die aber anders verteilt sind. Nimmt man denselben Generator für alle drei Farbkanäle, ergibt sich ein gröber gestricktes Schlierenmuster. Ich wüsste im Augenblick nicht, wie man hier einen mathematisch-statistischen Zugang erhalten könnte. Vielleicht hat der optische Augeneindruck ja auch keine Relevanz?
Verknäulte Fäden ('strings')
mit IrfanView
jpg-Format, komprimiert (50%)
Der Bildbetrachter IrfanView zeigt bei der höchsten Vergrößerungsstufe keine Pixelquadrate, sondern 'verschmiert' die Farben zu einem verschlungenen Knäuel von fadenartigen Gebilden, einer rechten Stringsuppe. Die jpg-Komprimierung - von 751 KB auf 45 KB! - verstärkt das Schlierenmuster etwas. Gezippt oder im png-Format bringt die Komprimierung fast nichts ...
Grübel
Es funktioniert? Aber ...
Mein Taskpool umfasste am Ende 8 Tasks, bei nur 4 Tasks stieg das Programm aus. Ein Wettrennen um Ressourcen?
AdaCore stellt für WinzigWeich Windows nur den 32-Bit-Compiler zur Verfügung. Und auf meinem 64-Bit-Rechner funktioniert der 32-bittige Debugger nicht. Ansonsten hätte ich wohl nach der Ursache geforscht :-)
Und: Ich habe versucht, das Bitmap-Datei-Format direkt durch einen Ada-Typ und Darstellungsklauseln abzubilden - es ist mir nicht gelungen ... was aber auch nicht so tragisch ist, denn das Speicherformat braucht ja nicht identisch mit den Verarbeitungsformaten zu sein.
Im Bitmap-Format mit 24-Bit-Farben werden die Farbdaten zeilenweise abgelegt, wobei auf die Doppelvwortgrenvze mit Nullbytes aufgefüllt werden muss. Der Verbundtyp Row_Colours_Record_Type (Zeilen 36-38) leistet dies etwa für 3 Pixel ohne weitere (mögliche) Vorgaben - was aber adatechnisch nur bei bekannter Pixelzahl so machbar ist. Eine weitergehende Parametrisierung scheiterte ...
(Man kann dem genannten Verbundtyp natürlich die Pixelzahl über eine Diskriminante mitteilen, die würde aber auch an dieser Stelle abgespeichert werden und da gehört sie nicht hin.)
Bitgenaue Positionierung im Speicher mit Darstellungsklauseln
Quellcode
Hinweis: Die Zeilennummern stimmen nicht unbedingt mit denen in den obigen Code-Ausschnitten überein.
• Der Quellcode, gezippt Mein GNAT-Projekt zum Weitermachen
Ada-Compiler
• GNAT-Ada-Compiler von AdaCore
Literatur
Das gute, alte Buch
John Barnes
Programming in Ada 2005
Addison-Wesley, 2006
Alan Burns, Andy Wellings
Concurrent and Real-Time Programming in Ada 2005
Cambridge University Press, 2007
Dank an die Herren Burns und Wellings
- für die Gedankenarbeit!