So, also zur Beortung.
Wir kommen jetzt zur Implementierungsphase.
Wir sind also mit dem Design erstmal durch.
Zur Übersicht kennen Sie ja noch unser schönes Wasserfall-Diagramm.
Das ist die Implementierungsphase, wo wir sind.
Und eigentlich gehört zur Implementierung natürlich nicht nur das
Programmieren, sondern auch ein Teil des Testens, nämlich des Testens
der einzelnen Komponenten, die man schreibt.
Jetzt haben wir gleich mehrere gehabt.
So, jetzt gehen wir nochmal zurück.
Was sollen wir machen können?
Wir sollten jetzt die UML-Modelle umsetzen können in ablauffähigen
Code.
Wir werden kurz über Parallelprogrammierung sprechen, weil jeder
Rechner, den Sie heute in der Hand kriegen, sogar der Mobiltelefon,
ein Parallelrechner ist.
Bis jetzt haben Sie nur sequenziell programmiert.
Jetzt müssen wir also auch sehen, selbstkontrolliertes Programmieren
anwenden können.
Das können Sie jetzt noch nicht sagen, was es ist, aber es kommt dann.
Also wir haben einen Überblick, da sind wir gerade.
Abbildung von UML auf Code, das wird uns nicht sehr lange in Anspruch
nehmen.
Das ist ziemlich einfach.
Ich muss nämlich lediglich die Assoziationen umwandeln in Referenzen.
Und abhängig von den Kardinalitäten gibt es da unterschiedliche
Implementierungstechniken.
Es ist nicht so, dass mit einer einzigen Referenz es getan ist in der
Regel.
Dann kommt eben Parallelität und selbstkontrolliertes Programmieren.
Und Programmierrichtlinien werden wir noch ganz kurz streifen.
Die kennen Sie eigentlich schon aus der Programmierseite her.
Jetzt probiere ich nochmal.
Es geht nochmal.
In der Implementierungsphase, eigentlich wollte ich Ihnen dieses Bild
hier geben.
Also in der Implementierung, das ist nochmal der Wasserfall.
Sie liefern ab Programme und natürlich dann dadurch auch letztendlich
die ganzen Komponenten zusammengefügbar zu einem Gesamtprodukt.
Sie bekommen also rein die Softwarearchitektur, in der Regel ein UML
-Diagramm.
Spezifikation der Systemkomponenten, die natürlich aus dem Diagramm
nicht hervorgehen, was die einzelnen Funktionen tun sollen.
Und dann arbeiten Sie wie ein Wilde oder ein Wilder und erzeugen
Quellprogramme inklusive integrierter Dokumentation.
Sie liefern auch die Objektprogramme, das heißt die übersetzten
Quellprogramme.
Das kann natürlich auch Übersetzung in virtuellen Maschinencode sein
oder direkten Maschinencode, je nachdem was man hat.
Und die Testsuiten und das Testprotokoll.
Testsuiten kennen Sie auch.
Sie schreiben Tests, die sich selbst ausführen und sich selbst
überprüfen.
Sie sind zusammengefasst in Testsuiten und das Testprotokoll sagt
lediglich von den 2.576 Testfällen sind alle perfekt durchgelaufen
oder eben 90% oder so etwas.
Und eine Verifikationsdokumentation, falls Sie eine Verifikation
durchführen, aber das werden wir wieder nicht besprechen.
Gut, das hatten wir schon.
Das sind die Eingaben.
Was sind Ihre Aktivitäten?
Datenstrukturen, Algorithmen konzipieren und verwirklichen.
Strukturierung des Programmes durch geeignete Veränderungsebenen.
Das heißt, Sie beschreiben einen Algorithmus erst in groben einzelnen
Schritten und nehmen dann jeden einzelnen Schritt her und verfeinern
den in eine Folge von Anweisungen oder wieder in eine Reihenfolge von
Schritten, die für sich genommen wieder übersetzt werden müssen, in
eine Folge von Anweisungen.
Das nennt man die sogenannten Verfeinerungsebenen und dokumentieren
das.
Und natürlich müssen Sie die verwendeten Programmiersprachen in der
schreiben.
Dazu kommt noch Angaben über Zeit- und Speichereffizienz oder
Ineffizienz, je nachdem ob das ein Modul oder eine Klasse ist, die
sehr aufwendig ist und sehr lange Laufzeit hat.
Dann sagt man, das hat zum Beispiel quadratische Laufzeit oder
Laufzeit nlog n.
Meistens braucht man das nicht, jedenfalls nicht bei den Programmen,
die wir hier schreiben.
Die wirklich laufzeitintensiven Beispiele kennen Sie eher aus der
Algorithmentechnik.
Und dann natürlich dazu die Testplanung und Testfallerstellung.
Das allerdings verschieben wir auf das nächste Kapitel, weil ich die
gleichen Konzepte zur Testfallerstellung und welche Kriterien erfüllt
werden müssen, wenn man sagen kann, ich bin jetzt fertig mit dem
Testen, das machen wir dann im nächsten Kapitel.
Wir nennen das auch Programmieren im Kleinen, während das Erstellen
einer Architektur das Programmieren im Großen genannt wird.
Zur Testfallerstellung und Testplanung ist es so, dass wir natürlich
kein Programm mit allen möglichen Eingaben vollständig testen können.
Die Anzahl der möglichen Eingaben ist einfach so gigantisch,
astronomisch hoch, in den meisten Fällen schon einfach.
Die Programmiere haben eine Testfallmenge, die weit über die Anzahl
der Atome im Universum hinausginge.
Und wir können also auch mit dem schnellsten Rechner der Welt nie alle
Eingaben durchprobieren.
Ganz abgesehen davon, dass wir dann die Ausgaben ja auch noch
überprüfen müssten, ob sie stimmen.
Und das muss ich oftmals per Hand machen oder wieder mit einem
Programm.
Und auch das dauert dann Zeit.
Also es ist völlig aussichtslos, auch nur ein einfaches Programm mit
ein paar Schleifen vollständig zu testen.
Daher brauchen wir Testfallerstellungskriterien, die uns sagen, wann
wir genügend getestet haben.
Wir können mit Testfällen nie garantieren, dass kein Fehler auftritt.
Geht einfach nicht, weil wir eben nicht alles probiert haben.
Aber deswegen ist das Testen nicht dazu da, um die Korrektheit zu
beweisen, sondern um Fehler zu finden.
Um Fehler zu finden, das ist das Testen.
Dafür ist das Testen da.
Darüber werden wir aber noch genauer sprechen.
Und wenn ich also im Testen Fehler finden soll oder überhaupt testen
soll, dann muss ich auch mal ein Kriterium haben, das sagt, jetzt
reicht es.
Jetzt reicht es, jetzt hast du genügend probiert, um Fehler zu finden.
Oder muss ich morgen noch weiter testen, nächste Woche auch noch und
so weiter und so weiter bis in alle Ewigkeit.
Das wird keiner sagen, sondern Sie brauchen ein
Testvollständigkeitskriterium.
Die werden wir kennenlernen, Testvollständigkeit oder auch
Testüberdeckungskriterien, die uns sagen, es reicht mit der
Versuchung, Fehler zu finden.
Aber wir geben keine Garantie, dass keine Fehler mehr dabei sind.
Aber wie gesagt, das kommt nochmal genauer.
Gut, die Teilprodukte, die Sie abliefern, sind natürlich
Quellprogramme, integrierte Dokumentation, OPEC-Programme, übersetzte
Versionen davon, ausführbare Testfälle und das Testprotokoll.
Das hatten wir im Bild schon gesehen.
So, das war also der Überblick.
Und jetzt schauen wir uns das genauer an.
Eine Kleinigkeit habe ich noch nicht gesagt, nämlich, was mache ich
eigentlich mit Automaten.
Wie übersetze ich Automaten, das zeige ich Ihnen auch noch.
So, also jetzt geht es genauer.
Jetzt kommt also das Nächste, das ist einfach die Abbildung von UML
-Diagrammen.
Und dazu gibt es auch einen Abschnitt in dem Buch von Brügge, das
sollten Sie sich auch lesen.
Hier gibt es also eine Übersicht.
Ich glaube, ich muss heute da bleiben, wo ich bin.
Meine Batterie ist eigentlich gut.
Zunächst einmal werden Sie nicht in Ihrem ganzen Leben nur schön mit
objektorientierten Programmiersprachen zu tun haben, sondern
vielleicht auch mit sowas Grausligen als wie C.
Die Programmiersprache C, warum heißt sie C?
Weiß das jemand?
Als es einen Vorleifer gab, der hieß B.
Tatsächlich so.
Und wann wurde die entwickelt?
Weiß das jemand?
1970.
Ist immer noch in Betrieb, immer noch im Gebrauch.
Darum müssen wir kurz sehen, was wir damit machen, wenn wir uns schön
mit objektorientiertem Zeug fabriziert haben.
Und dann sollen wir plötzlich mit C programmieren, wo es das gar nicht
gibt.
Gibt es keine Vererbung, keine Klassen, keine Polymorphie.
All das gibt es nicht.
Das wird uns stark eingrenzen, was wir dann kompakt übersetzen können.
Aber wir können mal anfangen.
Zum Beispiel ist natürlich normalerweise eine Klasse sieht so aus,
dass sie vorne die Attribute haben und dann gefolgt von den Methoden.
Was mache ich damit?
Das gibt es nicht in C oder anderen standardmäßigen, imperative
Programmiersprachen.
Es bleibt Ihnen nichts anderes übrig, als so eine Klasse C zu
übersetzen in eine sogenannte Struktur.
Eine Struktur oder Struct in C.
Das heißt, da schreiben Sie nur die Attribute rein.
Die Methoden, die passen da nicht, das geht einfach nicht.
Wir könnten hergehen und mit Funktionszeigen arbeiten, aber das lassen
wir mal sein, weil das wird dann schon wieder so kompliziert, dass es
auch wieder keiner mehr macht.
Dann geht man halt zu C++.
Also haben Sie einfach normales C und das schreibt man so, dass man
sagt, ich definiere eine Struktur, die heißt jetzt C, die hat in
diesem Fall eine einzige Integerzahl in ihrem Bauch.
Ich kann dann Instanzen dieser Strukturen anlegen, indem ich sowas
sage, Strukt C und dann Namen dafür aufzähle.
Dann werden diese C1, C2 und C3 angelegt.
In diesem Fall eben genügend Platz für eine Ganzzahl.
Und so wie ich es jetzt geschrieben habe, werden diese Dinge
tatsächlich auf dem Keller angelegt.
Das heißt, auf ihrem Laufzeitkeller.
Und wenn die Funktion, in der das passiert, zurückkehrt, dann sind die
Dinge auch verschwunden.
Sie haben hier keine Zeiger, keine Referenzen, sondern das sind
einfach Pakete von Daten, die angelegt werden wie eine ganz normale
lokale Variable einer normalen Methode oder Funktion.
Zugriff auf das Attribut ginge dann zum Beispiel, wenn Sie die Instanz
C3 nehmen, Punkt A1.
Das passt schon mal, das ist so ähnlich wie im Objektorientierten.
Aber wir haben dann nachher gleich noch eine andere Notation.
Wir kriegen nämlich auch Referenzen.
Das ist so, dass ich in C tatsächlich unterscheiden muss, ob ich
lokale Variablen habe oder Referenzen darauf.
Nächster Punkt.
Es gibt noch eine andere Methode und die kommt dem Klassenleben näher.
Sie können eine Typdefinition angeben.
Eine Typdefinition, die auch nur eine Struktur ist.
Die wird als Typedef davor gesetzt.
In A1 und so weiter.
Hier auch nur mit einem einzigen Attribut.
Dummerweise, aber gut, man kann so viele hinschreiben, wie man will.
Und diesen neuen Typ nennen wir C.
Groß C.
Das ist jetzt tatsächlich so, dass wir eine Klasse.
Sie können auch davon Instanzen bringen.
Aber schauen Sie, das sieht jetzt so aus, dass ich nur das C
vorschreibe und nicht so wie vorhin.
Da musste ich ja strukt C und dann den Namen.
Ich mache es jetzt so.
Aber auch diese sind jetzt auf dem Keller angelegt, wie die anderen
lokalen Variablen.
Eine Methode.
Ich kann auch mit Referenzen arbeiten.
Das geht folgendermaßen.
Ich nehme jetzt diese Struktur C her und will einen Zeiger drauf.
Diese Sternchen vor einem Variablen bedeutet nämlich einen Zeiger.
Das C4 ist also ein Zeiger auf ein Objekt irgendwo.
Das ist natürlich jetzt noch nicht initialisiert.
Jetzt muss ich es auch noch initialisieren.
Und Sie sehen schon, dass das jetzt ein bisschen mühsamer wird.
Sie rufen eine Funktion auf, die heißt malloc.
Das heißt nämlich Memory Allocation oder Speicher Allozierung.
Und müssen dann sagen, wie viel Sie da wollen.
Dazu sagen Sie Size of C.
Size of ist ein Operator, der Ihnen gibt für eine Typdefinition, wie
viel Platz Sie dafür brauchen in Bytes.
Müssen Sie jetzt nicht wissen, wie viel das ist.
Das Size of liefert Ihnen das.
Das malloc gibt Ihnen lediglich einen Block von Speicher irgendwo auf
der Halde, der groß genug ist, um ein C reinzupacken.
Das wird also angelegt, reserviert und es gibt einen Zeiger zurück.
Jetzt weiß das malloc auch wieder nicht, was für ein Typ das ist.
Das heißt, Sie müssen noch einen Cast davor schreiben.
Das sagt hier, was da zurückkommt, ist ein Zeiger auf ein Stern.
Entschuldigung, ein Zeiger auf ein C.
Das Sternchen sagt ja Zeiger auf C.
Das malloc an sich liefert Void Stern zurück.
Das ist ein unbekannter Typ und Zeiger drauf, das liefe ich dir
zurück.
Daher muss man hier noch eine Typumwandlung oder einen Cast anlegen.
Da muss man einiges denken, aber im Prinzip geht es.
Nur, dass Sie jetzt natürlich ein Objekt bekommen, das ist auf der
Halde angelegt.
Sie haben einen Zeiger drauf, den Zeiger können Sie weitergeben.
Sie können mit dem Zeiger auch natürlich die Attribute ansprechen.
Aber wir haben immer noch keine Funktionen drin.
Aber das kriegen wir auch noch hin.
Wenn ich jetzt aber auf Referenzen eine Referenz habe und damit die
Attribute rausholen will, dann darf ich nicht mehr Punkt schreiben,
sondern muss dieses Pfeilchen schreiben.
Dieser Unterschied ist im C einfach nicht wegzuwischen.
Ich muss wissen, ob ich eine Referenz habe oder ein tatsächliches
Objekt.
Im einen Fall Objekt mit Punkt und sonst mit Pfeil.
So, was machen wir jetzt mit den Methoden?
Also wir haben wieder in einer Klasse eine Methode M, die hat ihre
Parameter und greift auf die Attribute der Elemente in der Klasse zu.
Wie übersetze ich das in eine C-ähnliche Sprache ohne
Objektorientierung?
Das geht so, dass Sie, wenn Sie diese Methode M so sehen in einer
Klasse, dann machen Sie daraus eine freistehende sogenannte Funktion.
Freistehen, das heißt, sie ist nicht mehr an irgendeine Klasse
gebunden, gekoppelt, sondern sie ist genommen für sich, aber Sie
müssen ihr das Objekt, an dem es arbeiten soll, als ersten Parameter
mitgeben und dann den Rest der Parameter.
Das heißt also sowas wie O.M, Klammer auf, Parameter, Klammer zu.
Ein Methodenaufruf wird jetzt zu einem neuen Aufruf, der heißt ohne O,
einfach nur M, Klammer auf, O als erstes Parameter.
Übrigens, so wird auch der Aufruf in objektorientierten
Programmiersprachen übersetzt.
Wenn Sie also schreiben, Objekt, Punkt, Methodenaufruf, dann wird es
übersetzt in eine Methode, Klammer auf, mit dem Objekt als ersten
Parameter.
Das ist der Standard, so ist es zum Beispiel in C++ genauso.
Das müssen Sie halt jetzt per Hand hinschreiben.
Ist dann aber nicht mehr schwierig.
Und Sie sehen schon, das muss jetzt eine Referenz sein.
Sie könnten natürlich auch ein Objekt an sich übergeben.
Das wäre dann wieder ohne Referenz.
Das ist übrigens auch möglich, aber wir nehmen mal an, wir brauchen
eine Referenz dazu.
Und wenn Sie jetzt auf die Attribute des Objektes zugreifen wollen,
das Sie bekommen haben als Parameter, dann schreiben Sie halt Objekt,
Pfeilchen, Attribut.
Eigentlich ganz einfach.
Nur merken Sie, Sie haben keinerlei Vererbung jetzt.
Die Methode ist jetzt freistehend, keine Polymorphie.
Wenn Sie sagen, Methode M, Klammer auf, mit Objekt und so weiter, wird
genau diese Methode aufgerufen und nicht irgendwie eine vererbte oder
sonst was.
Wir können also zum Beispiel Schablonenmethoden nicht schreiben.
Oder nur mit Funktionszeigern, aber die spare ich mir.
So, jetzt schauen wir mal.
Also dieser Aufruf C4, eine Klasse C4, wo C4 eine Referenz ist, Punkt
M, wird zum Aufruf M, Klammer auf, C4 und der Rest der Parameter.
Das haben wir schon durchgesprochen.
So, jetzt kommen wir zur Vererbung.
Die Vererbung gibt es nicht, aber wir können sie simulieren.
Wir betrachten einfach die Vererbungshierarchie und sagen, okay, wenn
ich hier unten bin bei einer Klasse, die hat 25 Attribute und bekommt
nochmal 30 dazu vererbt, dann muss ich die einfach in der Deklaration
dieser unteren Klasse oder jetzt in der Struktur einfach
hinzukopieren.
Ich muss die Vererbungsarbeit eben per Hand schreiben.
Passiert nicht mehr dynamisch oder statische Ergänzung, sondern ich
muss es halt per Hand schreiben und wohlgemerkt natürlich nur für
Attribute, nicht für Methoden.
Aber dann kriege ich das schon hin.
Die Unterklassen oder die Implementierung der Unterklassen ist halt
dann ein bisschen aufwendiger.
Wie gesagt, Polymorphie haben wir nicht.
Auch die kann ich simulieren, wenn ich wirklich möchte.
Und das geht so, dass Sie sagen können, dass Sie aus so einer Methode
M auch noch einen Zeiger machen können, nämlich einen Zeiger auf eine
Methode und diesen umsetzen können.
Und dann können Sie alles machen, auch Kapronenmethode,
Kapronenmethode, Vererbung, Polymorphie, all das können Sie dann
implementieren.
Aber dann implementieren Sie eigentlich C++ nach.
Dann schreiben Sie halt gleich in C++ oder einer anderen
objektorientierten Sprache.
So weit, so gut.
Das ist also für den Fall, dass es daherkommt, dass Sie ein Praktikum,
eine Bachelorarbeit schreiben müssen und sagen, kein Java und keine
objektorientierte Sprache.
Wir arbeiten in C, wir sind auf niedriger Ebene.
Jetzt wissen Sie, wie Sie die Konzepte, die Sie vom Entwurf her
kennengelernt haben, auch umsetzen können in eine weniger tolle
Programmiersprache.
So, gibt es Fragen soweit?
Also beraten uns.
Aus den Klassen werden Strukturen mit den Attributen, die Methoden
werden freistehend.
Da haben wir als ersten Parameter ein Objekt oder eine Referenz auf
ein Objekt.
Und damit kommen wir schon ziemlich weit.
So, jetzt brauchen wir noch die Abbildung von Assoziationen.
Das ist auch keine große Zauberei.
Aber wir gehen jetzt zurück zu einer objektorientierten Sprache,
nämlich Java und sehen uns mal das an, wie das geht.
Also nehmen wir an, wir haben sowas wie Person und Konto und es gibt
eine Rolle, die heißt Gehaltskonto.
Sie sehen hier, es steht eine 1.
Das heißt, eine Person hat in diesem Falle, das ist natürlich komisch,
aber wenn wir es so haben, dann gibt es eben nur Kardinalität 1.
Wie erreiche ich die?
Und der Umkehr, also die Traversierung der Assoziation von Konto zu
Person ist nicht notwendig.
Das ist gewissermaßen die einfachste aller möglichen Situationen, eine
Assoziation zu implementieren muss.
Und Sie sehen schon, wie Sie das machen.
Sie legen bei dieser Person natürlich eine Variable an, die heißt
Gehaltskonto und die ist vom Typ Konto und fertig.
Nicht ganz, nicht ganz, denn hier steht eine 1.
1 heißt nicht 0, immer 1.
Das heißt, Sie müssen nur noch ein zusätzliches Teil machen, nämlich
hier haben Sie das Konto Gehaltskonto.
Wenn Sie die Person anlegen, instanziieren, eine Instanz davon
erzeugen, dann müssen Sie sich ja gleich ein neues Konto besorgen und
das Gehaltskonto setzen, sodass es nie 0 ist.
Denn das sagt die 1.
Es ist immer 1, genau 1.
Auch kurz nach der Initialisierung ist es bereits 1.
Nur an das müssen wir denken und dann kann ich natürlich auch das
Gehaltskonto wieder zurückgeben mit dem Gib Gehaltskonto oder Get
Account oder in welcher Sprache Sie auch programmieren, ist mir egal.
Sie können jetzt auch noch privatisieren das Gehaltskonto, damit es
nicht zufälligerweise von einer anderen Stelle noch modifiziert wird
und zum Beispiel auf 0 gesetzt wird.
Nehmen wir an, Sie haben eine 1 zu 1 Relation, dann ist also die
Umkehrung möglich.
Das ist auch ganz einfach.
Dann muss ich halt hier und hier einen Zeiger einsetzen, eine Referenz
einsetzen, hier aufs Gehaltskonto und hier auf die Person und dafür
sorgen, dass sie immer gesetzt sind.
Immer gesetzt heißt, ich muss dafür sorgen, dass wie ich auch
initialisiere, das Gehaltskonto zur Person gehört und umgekehrt, wenn
ich ein Gehaltskonto initialisiere, dann trifft das Gleiche zu.
Das braucht jetzt immer eine Person.
Denn da steht 1 zu 1.
Immer heißt auch, wenn das Gehaltskonto erzeugt wird, muss ich dazu
eine Person finden, die dazu passt.
Und das machen wir uns einfach.
Wir machen folgendes.
Wir können natürlich sagen, Class Person, hier steht das Gehaltskonto
und hier ist der Konstruktor.
Der sagt natürlich, das Gehaltskonto wird jetzt zu einem neuen Konto
gesetzt.
Und wohlgemerkt, hier steht ein This.
Das heißt, ich rufe den Konstruktor für Konto auf und sage ihm auch
gleich, wer die Person ist, auf die er verweisen soll.
Zurückzeichnen.
Und beim Konto mache ich das Gleiche.
Ich habe ein Konto, das sagt, ich brauche einen Besitzer.
Der Konstruktor.
Es gibt nur diesen Konstruktor.
Den setze ich mir natürlich und kann ihn dann zurückholen.
Nullwerte haben wir also vermieden.
Gleich der nächste Schritt.
1 zu n Assoziationen.
Also 1 auf der einen Seite.
Hier zum Beispiel 0 bis 1.
Und viele auf der anderen Seite.
Also ein Sternchen.
Da geht natürlich das jetzt so, dass ich hier mit einer einzelnen
Referenz nicht mehr zurechtkomme.
Bei der Person, die nur zu einer oder keiner Firma gehören soll,
reicht natürlich eine einzelne Referenz.
Also brauche ich hier irgendwo ein Mengenattribut.
Zum Beispiel eine Liste.
Und das sieht jetzt so aus.
Moment, jetzt muss ich nochmal kurz schauen.
Also, wir machen wieder eine klasse Person.
Die hat eine Referenz auf eine Firma.
Klar, die brauche ich.
Hier ist der Konstruktor.
Der muss jetzt gar nichts tun, denn am Anfang darf die Firma leer
sein.
Wenn ich jetzt aber die Firma setze, die Firma einfüge, dann muss die
Person auf die Firma zeigen und die Firma muss auf diese Person
zurückzeigen können.
Ich muss das jetzt an zwei Stellen modifizieren.
Ignorieren Sie mal die ersten beiden Anweisungen.
Ich sage lediglich, Entschuldigung, die erste Anweisung.
Die dürfen Sie erstmal ignorieren.
Ich setze also die Firma, die ist mein Parameter geworden von Setze
Firma.
Und dann sage ich, okay, dann gibst du einen Rückruf rüber auf die
andere Seite an die Firma.
Und jetzt musst du mich natürlich auch einstellen.
Also rufe ich dort stelle ein, das auf.
Alles klar.
Das hier ist aus einem anderen Grund notwendig, den Sie gleich sehen
werden.
Bei der Firma muss ich jetzt ähnlich machen, nicht ganz gleich, aber
ähnlich.
Ich brauche jetzt natürlich eine Private Collection vom Typ Person.
Die nenne ich meine Angestellten.
Und initialisiere zum Beispiel mit einer Arraylist.
Könnte ja auch mit einer Liste oder einer Menge initialisieren.
Das wäre also auch okay.
Ist aber natürlich noch leer.
Und der Public Firma, der Konstrukteur muss gar nichts tun, denn die
Firma kann natürlich auch niemanden haben.
Es war ja ein Stern dran.
Und die Funktion stelle ein, gibt eine Person, die sagt, ich muss
jetzt zu dieser Arraylist hier Angestellte was hinzufügen.
Nämlich diese Person, die als Parameter reinkam.
Und setze die Firma zu this.
Jetzt sehen Sie auch, warum ich diese Anweisungen hier brauchte.
Die brauche ich jetzt, um eine endlose Rekursion zu vermeiden.
Ich hätte zum Beispiel hier sagen können, ich setze meine Firma, dann
rufe ich Stelle ein.
Stelle ein sagt, okay, wunderbar, ich addiere die Person und dann rufe
setze Firma auf.
Und setze Firma sagt, okay, setze die Firma zu this, rufe Stelle ein,
Stelle ein, da habe ich einen ewigen Kreislauf.
Und das brechen die beiden Dinger hier ab.
Wenn die Firma schon gesetzt ist, diese Firma schon gesetzt ist, dann
muss ich nichts mehr machen.
Das bricht also die zweite Rekursion.
Dann kommt erst der Aufruf, dann der Aufruf, dann kommt ein Aufruf
zurück und der stoppt.
Und ähnlich hier.
Referenz hinzufügen, das macht es.
Endlosschleife verhindern.
Aber dieser Code hat noch einen Fehler.
Also so wie er da steht, hier ist nicht die Frage, ob ein Semikolon
oder eine Klammer fehlt oder sowas.
Sondern es fehlt noch eine logische Geschichte.
Genau, genau, genau richtig.
Wenn ich also schon eine Firma gesetzt habe, hat der Kollege gut
erkannt, aber ich wechsel die Firma, das heißt, ich setze Firma ein
zweites Mal auf, dann läuft natürlich alles wunderbar.
Die Firma wird umgesetzt.
Die neue Firma wird auch informiert, dass ich jetzt da bin, bei der
Firma angestellt bin.
Aber die alte Firma hat immer noch einen Verweis auf diese einzelne
Person, auf mich.
Ich bin also dann plötzlich bei zwei Firmen angestellt.
Und das ist genau der Fehler.
Und ich denke, Sie haben genügend Kenntnisse, um diesen Fehler auch
noch auszuprogrammieren.
Aber Sie sehen, dass man hier vorsichtig sein muss mit den
Assoziationen.
Ich muss immer an beide Seiten denken.
Und transaktional denken, das sage ich auch gleich.
Noch ein Punkt.
Ich muss also durch die ganzen Kardinalitätsfehler, aber wir haben es
jetzt bald, durchgehen.
Was habe ich bei Stern?
Stern, naja, dann brauche ich an beiden Seiten ein Mengenattribut, ein
mengenmäßiges Attribut, eine Railliste zum Beispiel, eine Menge.
Dann sind das also beidesfalls gestellt Collections, die ich zum
Beispiel als Raillist initialisieren könnte.
Code haben wir jetzt keinen.
Doch, haben wir auch einen.
Code geht ähnlich.
Ich habe eine Füge hinzu Arbeitgeber.
Wenn ich also mehrere Arbeitgeber habe, dann ist das natürlich der
Arbeitgeber.
Wenn ich schon dabei bin, dann kann ich aufhören.
Andernfalls füge ich den Arbeitgeber hinzu und rufe Stelle ein am
Arbeitgeber auf.
Ja, das F-Punkt.
Ja, dieses F-Punkt.
Und hier ähnlich.
Ich füge den neuen Person hinzu und rufe den Rückruf, falls ich mit
der Firma beginne, auch auf.
Und die Abfragen, die drin sind mit den Ifs, die verbreiten immer die
Wiederholung oder die Enddurchschleifen.
Das ist also dann nicht mehr schwierig.
Und natürlich haben wir hier das gleiche Problem wie vorher.
Ich kann zwar zu mehreren Firmen gehören, aber wenn ich bei einer
anderen Firma ausgestellt werden soll, dann muss ich das auch
irgendwie berücksichtigen können.
Das heißt, ich müsste vielleicht eine Methode haben, die heißt
Ausstellen.
So, also Zusammenfassung.
Wenn Sie 0 bis 1 haben, eine Referenz 0 bis 1, Assoziation auf ein A
mit 0 bis 1, dann reicht es, auf der gegenüberliegenden Seite eine
Instanzvariable vom Typ A anzulegen.
Eine Referenz.
Auf der gegenüberliegenden Seite heißt hier, ja, an dieser Stelle.
Von hier nach hier, ja.
Wenn es eine 1 ist, dann ist klar, dann brauche ich eine
Instanzvariable vom Typ A auf diese Seite und den Code, der
sicherstellt, dass diese Referenz niemals 0 ist.
Wenn ich ein Sternchen habe, dann brauche ich eine Vielfachmenge, auf
der gegenüberliegenden Seite.
Wenn ich die Aggregation und so weiter, die Komposition habe, dann
brauche ich natürlich auch eine Vielfachmenge B in A.
Das ist jetzt gerade umgekehrt.
Hier sei das B, ja.
Das heißt, das A besteht aus vielen Bs.
Das heißt, ich muss hier eine Vielfachmenge einsetzen.
Und den genauso.
Also, wenn wir Assoziationen implementieren, dann muss ich immer die
Assoziationen als Ganzes betrachten.
Immer an beide denken.
An beide Enden denken.
Das nennt man Transaktionaldenken.
Eine ganze Transaktion durchführen.
Ich ändere jetzt die Relation, auf die die Assoziation beruht.
Also nochmal zur Erinnerung.
Assoziationen beschreiben Relationen.
Relationen sind Kreuzprodukte.
Von A und B.
Und die Kreuzchen in dem Kreuzprodukt zeigen an, ob eine Beziehung
besteht.
Und die Assoziation sagt mir lediglich, dass hier eine Relation
gebildet werden darf und was die Form dieser Relation ist, also welche
Kardinalitäten sie haben kann.
Sie operieren dann natürlich mit dem Programm, dass diese Relationen
implementiert und müssen sich da an die Regeln, die durch die
Assoziation im UML-Diagramm vorgegeben ist, an die müssen sie sich
halten.
Synchronisieren in Nebenläufigen Programmen ist auch notwendig.
Also wir haben hier gesehen, dass wir oftmals an beiden Enden einer
Relation oder Assoziation Daten modifizieren müssen, und zwar so, dass
am Schluss wieder ein konsistenter Zustand entsteht.
Wenn ich jetzt nur ein paralleles Programm habe, dann kann es sein,
dass ich an der Stelle was verändere, an dem an A etwas verändere,
dann an B etwas verändere und gleichzeitig funkt mir ein weiterer
Faden, der an den gleichen Objekten arbeitet, dazwischen und ändert
auch, während ich noch nicht fertig bin.
Dann kann es zu seltsamen Überlappungen führen, die dann zu komischen
Zuständen führen.
Ich habe zum Beispiel begonnen, die eine Seite auf A1, auf B1 zu
setzen und muss dann noch B1 setzen, damit es zur Rückseite auf A1.
Aber bevor ich die andere Seite anfassen kann, kommt ein zweites
Programm daher, ein gleichzeitig laufender Faden, der ebenfalls das A1
ändert und auf ein C zeigt und dann, bevor ich eine Chance habe, das B
zu verändern, stellt er schon das B auf das C.
Insofern wäre es wieder konsistent, wäre meine Änderung verschwunden.
Aber ich bin noch nicht fertig und dann komme ich daher und setze das
B auf das A zurück.
Dann haben wir also eine Situation, wo der eine auf das C zeigt und
der andere auf das A, wenn sie aufeinander zeigen müssten.
Das ist diese Verschränkungsmöglichkeit der Zuweisungen, auf die wir
Acht geben müssen, wenn wir parallel programmieren.
Da werden wir ja spätestens am Freitag dazukommen.
Aber das wäre so eine Situation, dass wenn Sie an Assoziationen
arbeiten, gewissermaßen die Relationen modifizieren, dann müssen Sie
dafür sorgen, dass kein Faden dazwischenkommt, bis Sie beide Enden
korrigiert haben.
Dann darf der nächste Faden kommen und sagen, ich brauche aber das.
Ich muss da noch einen hinzusetzen oder etwas muss ich doch noch
ändern.
Das ist dann unter Umständen wieder okay, aber nicht gleichzeitig.
Wie man das macht, das macht man mit dem sogenannten gegenseitigen
Ausschluss und dazu gibt es in Java ein Hilfskonstrukt, das heißt
Monitor.
Wenn Sie wollen, können Sie schon mal nachschauen, was dieser Monitor
tut.
Der garantiert eben, dass eine Anweisungsfolge garantiert nur von
einem Faden auf einmal ausgeführt wird.
Dann gibt es noch die Sonderfälle.
Bei den Assoziationen sagten wir, es kann auch viele sein, aber die
könnten geordnet sein.
Dann muss ich eine Instanzwahl vom Typ Liste her nehmen, die ist
natürlich geordnet, oder auch Array-Liste oder Vektor.
Wenn es unique ist, dann darf ich keine Multimenge nehmen, sondern
muss eine Einfachmenge nehmen.
Also brauche ich jetzt aus dem Typ Menge vom Typ A zum Beispiel ein
Hash -Set oder ein Tree-Set.
Das garantiert mir, dass immer nur einer da ist.
Und schließlich gibt es auch noch diese komischen Qualifizierer.
Bei den komischen Qualifizierern brauche ich eine Abbildung.
Das heißt, ich brauche eine Abbildung von dem Qualifizierer auf das
Objekt.
Und das mache ich mit einem Hash-Map, weil das funktioniert schön
schnell.
Und den Methoden für den Zugriff über den Qualifizierer, das heißt,
ich greife auf etwas zu und sage noch zusätzlich einen weiteren
Parameter, der sagt, es gibt hier viele, ich will einen davon und der
Qualifizierer wählt den aus.
Das mache ich über ein Hash-Map, das sagt, ich nehme diesen
zusätzlichen Parameter als Schlüssel in die Hash-Tabelle rein, um
dieses eine Objekt, das sie wollen, zu identifizieren.
Wenn es existiert.
Also schauen wir uns das mal am Beispiel an.
Wir haben also eine Bibliothek und ein Buch und über eine Signatur
kann ich immer ein einzelnes Buch auswählen.
Also die Bibliothek hat natürlich viele Bücher.
Über die Signatur ist mir sichergestellt, dass ich immer nur eines
bekomme.
Das ist also der Qualifizierer.
Wenn Sie den Qualifizierer wegliesen, müssen Sie hier ein Sternchen
hinschreiben.
So, wie mache ich das?
Ist auch nicht schwierig.
Man muss eben nur mal gesehen haben, wie ich das mache.
Also hier ist meine Bibliothek.
Hier ist meine Funktion, die mein Buch zurückgibt und gibt Buch, sagt
einfach, ich brauche aber noch eine Signatur.
Und was mache ich dann?
Ich schaue mir die Bücher an.
Ich muss schauen, was Bücher sind.
Das ist ein Map.
Das gibt mir...
braucht eine Zeichenreihe, das ist der Schlüssel und ein Buch.
Und ich sage mir einfach Bücher, hole mir das raus, das gerade die
Signatur hat.
Ja, nochmal.
Wir haben ein...
eine Büchermenge.
Das ist ein Hashtag.
Und da speichere ich Bücher, die identifiziert sind über eine
Zeichenreihe, das ist die Signatur.
Ja, das ist einfach nur eine Zeichenreihe.
Ich kann etwas hinzufügen.
Ich kann ein Buch hinzufügen.
Da muss ich das Buch natürlich angeben und die gewünschte Signatur.
Ich muss jetzt also den Qualifizierer, den eindeutigen Qualifizierer
mitliefern.
Da kann ich mir natürlich einen Signaturgenerator vorstellen, der das
macht.
Der das weiterzählt zum Beispiel.
Und das Einfügen geht über Put, Signatur und B.
Und das Hinzufügen hinzu, Bibliothekssignatur, das geht so rum.
Also ich muss beim B Füge hinzu, Bibliothekssignatur, das noch füllen.
Das Füge hinzu, Bibliothek haben wir jetzt nicht implementiert, aber
dann funktioniert das Buch über die Signatur.
Also merken, Qualifizierer, Hashmap benutzen.
Und dann kommen sie schon wieder drauf, wie es geht.
Ja, jetzt kommt noch eine Sonderfrage.
Bevor wir weitergehen, gibt es dazu eine Frage?
Okay, dann gehen wir zum nächsten.
Das war eben diese Terne, Entschuldigung, jetzt haben wir erstmal noch
angenommen, Sie haben eine Assoziationsklasse.
Da hat sich jemand Mühe gegeben und ein ganz tolles Modell
geschrieben.
Da hängen auch noch Daten an den Assoziationen dran.
Entschuldigung, an den Relationen, an den einzelnen
Relationseinträgen.
Das sind die hier, zum Beispiel Sportler nimmt in Wettkampfen teil und
erzielt für jeden ein bestimmtes Ergebnis.
Und hier wollen sie die Platzierung rausbekommen, zum Beispiel erster,
zweiter, dritter und so weiter.
Das wollen sie jetzt realisieren.
Das ist natürlich so, dass ich das Ergebnis nicht beim Sportler
einsetzen kann.
Denn der Sportler mit einer einzelnen Variable, das geht nicht, der
nimmt an vielen Wettkämpfen mit unterschiedlichen Ergebnissen teil.
Und ich kann es nicht am Wettkampf teilnehmen, weil am Wettkampf viele
Sportler teilnehmen, die auch unterschiedliche Ergebnisse haben.
Also hier kann ich es nicht abspeichern.
Da muss ich mir was überlegen.
Und das mache ich so, indem ich das hier, wie wir schon mal gesehen
haben, mit zwei Relationen realisiere.
Ich sage, der Sportler hat mehrere Ergebnisse, der Wettkampf hat
mehrere Ergebnisse, aber jedes Ergebnis gehört zu genau einem Sportler
und einem Wettkampf.
Also ich bin beim Wettkampf A Platz 2 und es muss einen anderen Platz
haben.
Ich simuliere das also mit zwei Assoziationen, das geht dann ganz gut.
Wenn ich also jetzt hier ein neues Ergebnis eintrage, muss ich
natürlich dafür sorgen, dass das Ergebnis, das zugehörige Ergebnis
richtig gesetzt ist, also ein neues Ergebnisobjekt angelegt wird und
korrekt eingebaut wird.
In diese zwei Assoziationen reicht es also nicht mehr.
Wenn ich nur hier das Ergebnis angebe, dann fehlt mir der Wettkampf.
Okay, so weit so gut ist auch keine Zauberei.
Ja, dann muss das halt, wie soll ich sagen, irgendwie aus dem FF
können.
Naja, das ist ganz normal, das schreibe ich hin, ich weiß, wie das
geht.
Das ist einfach die Umsetzung der Idee der Relation.
So, Sie haben schon gesehen, jetzt kommt was ganz anderes, nämlich
Zustandsautomaten.
Das ist jetzt nochmal ein Rückgriff, aber erst noch hierzu, ob es zur
Umsetzung noch Fragen gibt.
Und, das Funken funkt manchmal nicht richtig.
Gibt es noch Fragen?
Okay, dann machen wir es so.
Jetzt kommt noch etwas, das hatte ich Ihnen vorher noch nicht gesagt,
nämlich Zustandsautomaten.
Sie erinnern sich, ich hatte immer gesagt, bei den Entwurfsmustern,
das überspringen wir, weil ich es in der Implementierung zeige, da
muss ich ein bisschen Code dazu nehmen, das passt hier besser.
Darum zeige ich Ihnen das hier.
Also, wir sagten ja, im Zustand ist es so, dass Sie Objekte haben,
wenn Sie in einem bestimmten Zustand sind, dann benehmen Sie sie in
diesem Zustand immer gleich.
Wenn Sie im anderen Zustand sind, dann führen Sie andere Aktionen
durch.
Wir wollen das also jetzt sehen, wie wir das sehr schick, muss ich
wirklich sagen, sehr schick implementiert.
Also das Erste ist, woher weiß ich, was im Zustand ist?
Also, da gibt es die implizite und die explizite Speicherung.
Das ist eigentlich keine Zauberei wieder einmal.
Implizite Speicherung sagt lediglich, ich kann aus vorhandenen
Variablen ablesen, was der Zustand ist.
Zum Beispiel, ob etwas schon initialisiert ist oder nicht.
Das kann ich vielleicht an einem Nullzeuger ablesen.
Da brauche ich keine zusätzliche Variable, die mir das sagt.
Weil es ja schon da ist.
Oder die explizite Speicherung, die sagt, ich mache einen
Enumerationstyp auf, der zählt alle Zustände auf, gebe dann dem Objekt
eine Variable und die nimmt also als Werte einen dieser möglichen
Zustände an.
Und damit kann ich explizit angeben, was der augenblickliche Zustand
ist.
Denn den brauche ich, um zu unterscheiden, was zu tun ist.
Also hier haben wir so ein einfaches Beispiel.
Sagen wir, wir haben ein Objekt, kann instanziiert sein, das heißt, es
kann angelegt sein.
Oder es kann auch instanziert und dann initialisiert worden sein.
Das heißt, die Daten sind jetzt richtig.
Ich komme von diesem Zustand in diesen Zustand, indem ich die init
-Funktion aufrufe und etwas zuweise, dem PrintStream zuweise.
Und im Zustand initialisiert bleibe ich natürlich, wenn, das ist ein
Logbuch zum Beispiel, wenn ich die anderen Funktionen aufrufe, die
einfach irgendwas zu diesem Objekt hinzufügen.
Dann bleibe ich in diesem Zustand.
Das ist ein ganz einfacher Automat.
Zwei Zustände, zwei Übergänge.
Und Sie sehen, ich kann das jetzt so machen, das ist ein Logbuchführer
oder ein Logger.
Der sagt, er hat einen PrintStream-Log und da gibt es eine Funktion,
die initialisiert, das ist die hier.
Da lege ich mir das Log einfach auf diesen PrintStream.
Auf dem drücke ich das Logbuch, die Logbuch-Einträge aus.
So weit, so gut.
Dann ist die Funktion Log, die sagt, falls das Logbuch eben hier noch
nicht gesetzt ist, dann gibt es eine Ausnahme.
Das heißt, IllegalStateException aus Java, Punkt lang.
Haben Sie vielleicht noch nicht benutzt.
Und andernfalls füge ich das per Append diese Botschaft an den Strom
mit an.
Alles so weit, so gut.
Und jetzt kann ich natürlich sagen, Entschuldigung, jetzt bin ich zu
weit gegangen.
Das hier sagt mir natürlich, welchen Zustand ich bin.
Wenn das nämlich noch Null ist, dann bin ich in diesem Zustand.
Wenn es nicht mehr Null ist, dann bin ich in dem Zustand.
Braucht keine zusätzliche Zustandsvariable.
Aber wenn ich das wollte, könnte ich das natürlich machen.
Jetzt.
So.
Gewollt jetzt mit Enumeration und expliziter Variable.
Sie sehen hier die ersten beiden Zeilen der Klasse Logger.
Das sieht so aus, dass ich hier eine Enumeration angebe.
Zustand.
Das zeugt mir also einen neuen Typ, der genau diese zwei Variablen,
diese zwei Werte annehmen kann.
Instanziert und Initialisiert.
Ja.
Sie sehen fast genau auch.
Sonst habe ich hier eine Variable.
Die ist privat.
Die heißt Zustand.
Ist zum Typ Großzustand.
Und wird Defaultmäßig gleich bei der Instanzierung auf Instanziert
gesetzt.
Steht nicht mal im Konstruktor.
Sondern der steht einfach gleich am Attribut dran.
Ist auch erlaubt.
Könnte ich natürlich auch in den Konstruktor verlegen.
Ist dann vielleicht offensichtlicher.
Aber auch so geht es.
Und dann gibt es den Log Print Stream.
Und der sagt Init.
Wenn ich das Ding aufrege, dann gehe ich ja in den Zustand
Initialisiert.
Das setze ich mit einem Log Bucheintrag und ende den Zustand auf
Initialisiert.
Und der hier kann jetzt den Zustand direkt abfragen.
Ist das nicht initialisiert, dann gibt es Theater.
Da werfen wir eine Aufnahme.
Ansonsten geht es nochmal weiter.
Also explizit, implizit.
Das ist hier so einfach, dass ich eigentlich sagen würde, die vorige
Lösung reicht auch.
Die implizite Lösung, weil es nur darum geht, um eine Variable zu
betrachten, ob sie Null ist oder nicht.
Die implizite Speicherung spart natürlich Speicherplatz.
Ich brauche diese Zustand, diese eine zusätzliche Variable natürlich
nicht.
Wie es in diesem Fall natürlich nur ein Byte gewesen wäre.
Also was ist das schon?
Die implizite Steuerung ist dafür komplizierter.
Ich muss eben ablesen können.
Das war in unserem Beispiel ganz trivial.
Es muss nicht trivial sein.
Angenommen, Sie wollen wissen, ob eine Person alt ist.
Dann müssen Sie wahrscheinlich prüfen.
Nämlich das alte Prüfen muss den Wert festlegen, ab wann jemand als
alt betrachtet wird.
Das heißt, da müssen Sie ein bisschen rechnen, um aus dem impliziten
Zustand den impliziten Zustand abzulesen.
Das dauert dann natürlich etwas.
Wenn dann eine Variable da wäre, die immer auf den aktuellen Stand
gehalten wird, was auch so ein Problem ist, weil sich die Zeit ja
ändert.
Plötzlich wird er von jung auf mittelalt auf ganz alt.
Das müssen Sie jeden Tag alle Personen durchforsten, um zu sehen, ob
es da eine Zustandsänderung gab.
Wäre es auch ungeschickt.
Aber im Prinzip ist es so.
Die Explizitität wird immer offensichtlich und es ist eher
wahrscheinlich, dass bei Änderungen diese Zustände nicht vergessen
werden.
Also wenn Sie neue Fälle einführen, neue Methoden einführen, die die
Zustände unterscheiden müssen, dann ist bei einem expliziten
Darstellung die Wahrscheinlichkeit höher, dass Sie sagen, oh, welche
Zustände gibt es denn da noch?
Die sehen dann eben explizit da und dann können Sie sich fragen, ob
Sie in diesen Fällen noch zusätzlich einen Code schreiben müssen.
Also wird es weniger oft.
Die implizite Speicherung ist nicht immer möglich, die explizite
können Sie schon immer machen.
So.
Und jetzt kommt die eingebettete und die ausgelagerte Technik.
Und das ist eigentlich die interessante, die ausgelagerte.
Und die ist jetzt ein bisschen kompliziert.
Mal überlegen, wie ich am besten anfange.
Ich fange mit diesem Beispiel an.
Also.
Wir machen jetzt die eingebettete explizite Speicherung.
Das heißt, es gibt eine explizite Zustandsvariable.
Okay?
Und wir haben diesen Zustandsautomaten mit diesen drei Zuständen.
X, Y und Z.
Und die Übergänge sind C, B und A.
Sie sehen, das C kommt zweimal vor, das B kommt zweimal vor, das A
kommt nur einmal vor.
Das heißt, in den anderen Fällen, also zum Beispiel hier, wenn Sie
hier den Aufruf A hätten, dann müsste es eine Ausnahme geben.
Das ist nicht erlaubt in diesem Zustand.
Das nächste, was wir natürlich klar sehen müssen hier ist, dass die
Zustandsübergänge ausgelöst werden durch Methodenaufrufe.
Sie haben ja die Zustandsautomaten kennengelernt als eine Eingabe.
Es passiert eine Eingabe, ein Zeichen kommt rein und bumms, der
Zustand ändert sich mit dem Zeichen.
So sieht das hier nicht aus, sondern wir nehmen an, dass alle
Zustandsänderungen nicht durch Lesen von Eingaben geschieht, sondern
durch Aufrufe.
Das heißt also, die Eingaben sind eigentlich Methodenaufrufe oder
Nachrichten, die Sie an dieses Objekt schicken.
So ist das die schöne Schreibweise oder Sprechweise für den Automaten.
Sie schicken eine Nachricht und diese Nachricht ist natürlich ein
Methodenaufruf, den Sie ausführen und der verursacht gegebenenfalls
einen Zustandsübergang.
Und das ist jetzt hier implementiert, zum Beispiel die Methode A.
Die Methode A sagt also, falls der Zustand X ist, falls der Zustand X
ist, dann ist es erlaubt.
Moment mal, ich muss nochmal schälen.
Dann ist es erlaubt und der zukünftige Zustand, der nächste Zustand
ist das Z.
Und dann bin ich auch schon fertig.
Was jetzt noch fehlt ist natürlich, dass ich für den Fall, dass B geht
auch, C geht auch, Sie muss die ganzen Fälle hinschreiben und es geht
so weiter.
Nämlich, falls der Zustand Y ist, dann ist es nicht erlaubt.
Falls der Zustand Z ist, dann ist es erlaubt.
Dann kann ich...Moment...
Nein, dann auch nicht.
Also in X darf der Zustand, der Aufruf A kommen, in Z und Y nicht,
also habe ich diese Fallunterscheidung, es ist X, es ist Y und es ist
Z.
Nächste Methode, B sagt, ist es X, ist es Y oder ist es Z.
Sie können nachvollziehen, wenn ich im Zustand X bin, ist das B
erlaubt und der nächste Zustand ist das Y.
Wenn ich im Zustand Y bin, das ist der hier, dann ist kein B erlaubt,
also gibt es eine Ausnahme und das Z ist, dann darf das B wieder
kommen und ich muss als neuen Zustand das Y angeben.
Und das C geht genauso.
Sie sehen also jetzt If X, If Y, If Z.
If X, If Y, If Z.
Wenn ich natürlich schöner hinschreiben mit einer Switch-Anweisung,
die sagt, Zustand, geschweift die Klammer auf, Case X, Case Y, Case Z.
So, jetzt wo Sie leitgeprüfte
Entwurfsmuster Kenner sind, an was erinnern Sie das?
Da hatten wir mal was gesagt, ja?
Bitte?
Ja, genau.
Gut, das haben Sie gut leise gesagt.
Da kann ich noch jemanden sehen, ob es noch jemand anderer weiß.
Genau, genau, so hatten Sie auch gesagt.
Also die Strategie ist das Muster.
Switchless Programming, Sie erinnern sich?
Ich habe Methoden, die haben immer die gleichen Äste.
Habe ich hier immer die gleichen Äste?
Ja.
Es ist halt nicht das Switch hingeschrieben, sondern das If.
Was mache ich?
Ach, wie war das noch mal?
Ich nehme einfach diese Klasse her und zerlege sie in drei.
Eine Strategie mit einer Oberklasse und drei Unterklassen, die haben
X, Y und Z drin.
Und da stehen dann für zum Beispiel eben den Zustand, der für den Z
-Zustand steht, da steht für A der Code für was im Zustand Z zu tun
ist und nicht die anderen Äste.
Sondern nur der eine Ast.
Und in B genauso, in C genauso.
Ich habe also jetzt mehrere Objekte, die ihre eigenen, Entschuldigung,
mehrere Klassen, die Unterklassen einer gemeinsamen Strategie
-Oberklasse sind und die jeweils richtigen Code enthalten für den
Zustand, für den sie zuständig sind.
Kann das funktionieren?
Es funktioniert.
Ich zeige es Ihnen gleich noch im Detail.
Es muss allerdings noch ein weiteres Muster eingeführt werden, nämlich
die Schablonen-Methode.
Die Schablonen-Methode brauche ich auch noch, um den Zustand, den
neuen Zustand zu setzen.
Aber das kommt gleich.
Also es ist dann eigentlich eine Kombination von Strategie und
Schablone.
Okay.
Das habe ich Ihnen jetzt alles gesagt.
Also, hier ist das C, die ursprüngliche Klasse, hat drei Methoden.
Wir hatten sie vorhin A, B, C genannt.
Das sind nämlich M1, M2 und M3.
Ist egal.
Die hatten also diese Verzweigungen drin, die unterscheiden nach den
verschiedenen Situationen.
Und das will ich jetzt auseinanderklamüsern.
Also sage ich, gut, ich mache mal einen C-Zustand, das ist der
ausgelagerte Zustand.
Ignorieren Sie das erstmal.
Entschuldigung, ich muss noch mal weitergehen.
Der C-Zustand, das ist der hier, der sagt, ich habe jetzt drei
Unterklassen, nämlich den XYZ-Zustand, den Zustand XYZ.
Das sind einzelne Klassen.
Da stehen die Methoden M1, M2 und M3 drin.
Jeweils implementiert nur für den Zustand X in diesem Falle und hier
für den Zustand Y in diesem Falle.
Der Grund, warum M3 und M2 hier alleine stehen, werden Sie gleich
sehen.
Das hat damit zu tun, dass ich hier die Default-Implementierung
benutzen kann.
Auch noch eine schöne Schönheit dieses Musters.
Warum sind hier auch noch Methoden drin?
Naja, hier drin stehen die Methoden, die sagen, wenn was falsch ist
oder wenn das Standardverhalten da ist.
Zum Beispiel stehen hier Methoden M1 und M2 drin.
Die sind ja privat.
Die geben ja nichts zurück.
Wenn ich aufgerufen werde, werfe ich eine Ausnahme.
Ich werfe einfach eine Ausnahme.
So, der Zustand Y, jetzt schauen wir nach.
Achso, da steht ja der Zustand.
Also zum Beispiel im Zustand Y darf nur M3 aufgerufen werden.
Außerdem habe ich natürlich hier das M3 implementiert.
Ich muss das und das und das tun.
Zustand Y Eingabe M3 oder Aufruf M3 oder Botschaft M3, da muss ich was
tun.
In allen anderen Fällen ist ja ein Fehler.
Dann läuft natürlich das M2 oder das M1 und wirft die Ausnahme.
Ah, geschickt.
Auch wieder geschickt gemacht.
Im Zustand X ist alles möglich.
Da ist das M1 möglich, das M2 und das M3 möglich.
Also muss ich hier Implementierung für M1, M2 und M3 liefern.
So, was brauche ich noch?
Achso, Eintritt- und Austrittsmethoden.
Wir erinnern uns, die Herald-Automaten hatten ja noch die spezielle
Feinheit, dass wir beim Eintritt in eine Klasse eine Methode aufrufen
durften und beim Austritt aus einer Klasse gleich auf welchem Weg.
Also brauche ich dafür auch noch Methoden.
Die müssen natürlich auch noch aufgerufen werden.
Wo?
Das sehen wir gleich.
Ja.
Die werden nämlich im Zustandswechsel aufgerufen und die Default
-Implementierung und die vorbesetzte Implementierung sind leer.
Wir sagen, es gibt keine, nichts zu tun beim Eintritt, beziehungsweise
beim Austritt.
Wenn es was zu tun gäbe, dann müsste ich sie hier überschreiben.
Haben wir hier nicht, aber im Prinzip müsste ich hier die Entry- und
Exit -Methoden einfach überschreiben.
Mit tatsächlichem Code.
Der hier ist nur leere, sind leere Rümpfe.
Okay.
Was habe ich jetzt noch vergessen?
Achso, jetzt sieht das also so aus.
Und hier ist die eigentliche Klasse.
Von dem hier erzeuge ich ein Objekt und an den schicke ich die
Methoden M1, M2 und M3.
Und jetzt erwarte ich, dass der Zustand richtig gewechselt wird,
beziehungsweise die Methoden richtig aufgerufen wird.
Also brauche ich noch natürlich eine Zustandsklasse.
Entweder den da, ein Zustandsobjekt, entweder das, das oder das von
diesem Typ.
Naja, das merke ich mir.
Aktuell nenne ich das Ding Meinung, das ist mein aktueller Zustand.
Kann jeder von diesen C-Zuständen sein.
Der hier ist ja abstrakt.
Da ist eine abstrakte Methode drin.
Sehen Sie, die nächste ist kursiv geschrieben.
Die ist abstrakt.
Kann ich also gar nicht instanziieren.
Ich muss stattdessen eine von denen hier wählen.
Was machen also diese Methoden?
Das ist meine Klasse C.
Also ich spreche jetzt über diese Klasse.
Die hat einen geschützten Zustand.
Das ist aktuell.
Und wenn ein M1-Aufruf kommt, dann delegiere ich.
Ich sage einfach, aktuell Punkt M1.
Du mach.
Was immer der Zustand ist.
Und wenn das zum Beispiel der hier ist, dann sagt der Zustand M1.
Ich habe nichts M1 einziges, aber ich kriege was vererbt.
Das ist void.
Also würde in diesem Fall eine Ausnahme passieren.
In diesem Fall würde M1 laufen.
So weit, so gut.
Jetzt fehlt aber, ich delegiere also lediglich an dieses
Zustandsobjekt.
Und dann muss ich noch die Zustandsänderung machen.
Die müssen wir uns jetzt noch anschauen.
Achso, das Entry und Exit hatte ich schon erklärt.
Das können wir über die Folie überspringen.
Das sind die Standardimplementierung von M1 und M2 in den C-Zustand.
Ganz oben.
Nicht leer, sondern die Fehlerzustände.
Und jetzt müssen wir uns den C-Zustand nächster überlegen.
Es gibt eine abstrakte Methode, die heißt nächster.
Ich gehe nochmal zurück.
Wenn ich kann.
Noch eins.
Der hier ist abstrakt.
Der Oberzustand kann nicht wissen, was der nächste Zustand ist.
Das wissen nur die hier.
Da müssen die alle Nächste implementieren.
Die geben einen C-Zustand zurück.
Und welchen, das wissen sie nur sie, weil z.B.
ein M2 aufgerufen wird.
Dann ist der nächste Zustand für ein X das Y.
Andernfalls ein Z.
Also gut.
Die wissen das.
Die sagen aber nur, was diese Änderung ist.
Und dann die eigentliche Funktion, die den Zustandswechsel ausführt,
ist die hier.
Die ist dann auch ganz einfach.
Zustandswechsel, wie es nur einmal geschrieben ist, in der Allgemeinen
Oberklasse.
Das ist die Schablonenmethode.
Die sagt jetzt folgendes.
Wunderbar.
Wenn du Zustandswechsel aufrufst, das ist die vererbte Methode.
Die haben die einzelnen Zustandsklassen geerbt, unverändert,
unüberschrieben.
Dann rufe ich einfach Nächster auf.
Das ist der Hook, der Haken.
Das ist lokal implementiert.
Ohne Vererbung rufe ich Nächster auf.
Der sagt mir, was der nächste Zustand wäre.
Nächster Zustand, sagt er, gut, das merke ich mir erstmal, denn ich
muss da jetzt etwas überprüfen.
Der sagt dann, wenn ich jetzt in einen Zustand wechsle, der nicht der
gleiche ist, in dem ich gerade bin, ich bleibe also nicht in diesem
Zustand, dann müssten Exit- und Endreaktionen aufgeführt werden.
Und die rufe ich danach auf, nämlich an mir selbst rufe ich die Exit
-Methode auf, wenn sie implementiert ist.
Wenn sie nicht implementiert ist, rufe ich sie auch auf, aber das ist
die Leere, die tut mir nichts.
Und dann rufe ich beim Zielzustand die Entry-Methode auf, in Punkt
Entry.
Auch gut.
Und dann zeige ich noch Object.Aktuell, der neue Zustand, dann bin ich
fertig.
Ein Zustandsübergang.
Das sagt also erst, ich schaue mal nach, rufe M1, M1 zum Beispiel, das
wird aufgerufen, in einem bestimmten Zustand, das läuft ab und wird
delegiert.
Wenn die Methode des Zustands zurückkommt, sagt die C-Klasse selbst,
jetzt muss ich den Zustandswechsel aufrufen, und das sieht dann so
aus.
Also die Klasse C hat den aktuellen Zustand, delegiert an diese
Zustandsklassen, hat eine Methode M1, M2 und M3, so für alle, und die
sehen einfach immer gleich aus.
Die sehen auch wie aktuell Punkt M1, das heißt also, den delegierten
Zustand machen lassen, und dann den Zustandswechsel aufrufen, an
meinem Zustand, an dem aktuellen Zustand.
Und der setzt natürlich aktuell um.
Moment, Moment, da ist was verkehrtes gesagt.
Ne, das stimmt schon.
So, das wird jetzt nochmal in einem längeren Beispiel, oder an dem
Beispiel, das wir eben gehabt haben, nochmal komplett ausprogrammiert,
damit Sie das sich anschauen können.
Eigentlich ganz einfach, nur am Anfang unglaublich verwirrend.
Geben Sie zu.
Also, wir gehen nochmal an dieses, Moment, an dieses Bild hin.
Sie haben also die Ursprungsklasse C, die bekommt Botschaften und muss
in Abhängigkeit von der Botschaft was machen und den Zustand wechseln.
Das hängt immer vom Zustand ab.
Als Zustand speichern Sie sich jetzt explizit, aber nicht eine
einfache Variable, die nur drei Werte annimmt, sondern Sie merken sich
eine Referenz auf eine Klasse.
Auf ein Objekt einer Klasse.
Und dieses Objekt einer Klasse weiß, was in einem bestimmten Zustand
zu tun ist.
Das ist das Strategiemuster.
Das hier wäre der Kontext.
Das ist die Strategie.
Und den Zustandswechsel, den müssen wir uns halt nochmal genau
anschauen, ob der auch wirklich funktioniert.
Das ist die Schablonenmethode, die mit eingebaut ist.
Der Zustandswechsel ist der hier.
Der ruft nämlich Nächster auf.
Und der Nächste wird erst implementiert in eine Unterklasse.
Das müssen wir noch...
Sie meinen jetzt den Zustandswechsel.
Also...
Jetzt müssen wir nochmal genau hinschauen, wo der Zustand sitzt.
Also aktuell ist das hier.
Ich muss also, wenn ich den Zustand wechsle, diese Variable aktuell
modifizieren.
Ja.
Und...
diese Modifikation, wenn wir das genau anschauen, vielleicht habe ich
sogar einen Fehler gemacht, der Zustandswechsel, der macht genau das.
Achso, das hatte ich Ihnen nicht gesagt.
Jetzt verstehe ich.
Sie sehen, ich habe hier einen Rückwärtszeiger.
Für jeden Zustand sagt der, wo das eigentliche C auch ist.
Ja.
Ich könnte jetzt also einen Rückgabewert machen, muss ich aber nicht,
sondern ich setze das Ding direkt.
Sie sehen, wenn Sie jetzt wieder vorblättern, das zeigt objekt
.aktuell.
Das ist der Rückwärtszeiger vom Zustand.
Da ist das C.
Das sagt auch, was ich zeigen muss.
Und da sage ich einfach, das ist ein N.
Wo waren Sie jetzt?
Ist die Sache geklärt?
Nein, Sie meinen, wenn ich jetzt den Zustand zurückgehen wollte, kann
ich auch machen.
Dann müsste ich hier den Code umschreiben.
Moment, wo ist er?
Zustandsänderung fehlt noch.
Da, könnte ich hier machen.
Könnte ich hier zurückgeben und aktuell setzen.
Aber mache ich auch so rum.
Das ist noch eine Möglichkeit, wie es Ihnen besser gefällt.
Aktuell ergibt sich Zustandswechsel.
Geht auch.
So, dann haben wir dieses Beispiel gesehen.
Jetzt kommen wir zur Parallelität.
So, da kommt jetzt ein bisschen Einführung.
Warum eigentlich Parallelität?
Die meisten Leute fürchten sich ein bisschen davor.
In der Tat ist das jetzt eine Denkweise, die jetzt zum Tragen kommt,
die Sie jetzt noch nicht hatten.
Das war nämlich so zu tun, als gäbe es mehrere Fäden, die gleichzeitig
arbeiten, an den gleichen Variablen arbeiten und sich dabei ins Gehege
kommen.
Und zwar ist es so, dass in der Vergangenheit Parallelrechnen gibt es
eigentlich schon seit Ewigkeiten.
Weiß zum Beispiel jemand, was der erste oder die ersten beiden
Parallelrechner waren?
Wahrscheinlich nicht.
Das war eine Cray-Maschine, das war ein Vektorrechner.
Der konnte also mehrere Daten gleichzeitig verknüpfen, also Vektoren
zum Beispiel parallel addieren.
Oder es gab noch eine andere Maschine, die ist der ILLIAC 4.
Da gab es einen anderen Instruktionssatz.
Aber der Punkt war, wenn man mehr Leistung wollte, als die
gegenwärtige Hardware-Generation hergab, musste man immer schon
parallel rechnen.
Nur da, wo man das brauchte, war das hauptsächlich im
wissenschaftlichen Rechnen.
Wissenschaftliches Rechnen heißt, ich möchte zum Beispiel eine Crash
-Simulation machen im Rechner.
Das heißt, ich muss hier ein Gitter dieses Fahrzeugs bilden und dann
die Impulse berechnen und die Verformungen berechnen, die dieses
Fahrzeug dann verformen und dann sehen, was passiert.
Zum Beispiel einen schiefen Aufprall oder sowas.
Oder Sie wollen die Faltung von Proteinen ausrechnen.
Das ist einfach unglaublich aufwendig, weil Sie für eine
Proteinfaltung für jedes einzelne Molekül mehrere Anziehungskräfte,
Van der Waals-Kräfte, elektrostatische und ich weiß nicht, was da
alles berechnet werden muss und am Schluss bilden sich so komische
Gebilde.
Kann man heute ganz komplexe Moleküle berechnen und die Form der
Moleküle ist ja wichtig für das Andocken zum Beispiel von Viren oder
das Andocken von Medikamenten.
Oder vielleicht das Unendlichste von allen ist, Sie wollen simulieren,
wie Galaxien ineinander sich, wenn Galaxien aufeinandertreffen.
Das ist insofern unendlich, weil es nämlich unendlich viele Sterne
gibt und Sie da unglaublich viel rechnen können.
Und zweitens ist es sehr schwierig zu beobachten.
Sie können nicht einfach in den Himmel schauen und sagen, da treffen
zwei Galaxien auf und dann mal schauen, was da passiert.
Das dauert leider Jahrhunderte und so lange lebt kein Astronom.
Also müssen Sie das irgendwie im Zeitrafferverfahren ausrechnen und
die Ausrechnung, die da stattfindet ist, dass man sagt, ich nehme
jeden Stern her, der bewegt sich mit einer bestimmten Geschwindigkeit
in eine bestimmte Richtung und hat eine bestimmte Masse.
Auf diesen Stern wirken ein hunderttausend andere Sterne aus
verschiedensten Richtungen, aus verschiedensten Entfernungen mit
verschiedenen Massen.
Wie wird dieser einzelne Stern jetzt abgelenkt?
Für die nächste Simulationsperiode wird der Stern in diese Richtung
ein klein bisschen so rum abgelenkt.
Und dann rechnen Sie für jeden Stern diese hunderttausend Sterne aus.
Das ist eine n-quadratische Anwendung.
Das heißt, jeder Stern erfordert, dass n-1 andere Sterne besuchen und
Rechnungen durchführen.
Und das müssen Sie jetzt n-mal machen.
Typischer Fall.
Um da irgendwo hinzukommen, braucht man eigentlich einen
Parallelrechner.
Das Fußvolk, das sagen wir mal wir, die nicht Superrechner brauchten,
haben sich immer auf das sogenannte Murschi-Gesetz verlassen.
Und er sagte zum Beispiel, dieser Herr Muren, das ist hier ein Bild
von ihm, er sagte, dass die Komplexität der Minimum-Component-Costs
hat sich erhöht.
Also er sagt sowas,
er nimmt einen festen Komponente-Kosten her und sagt, dafür gibt es
alle alle zwei Jahre eine Verdopplung.
Nicht jedes Jahr, sondern jede zwei Jahre, das war aber schon 1975.
Und mit dem habe ich gelebt.
Ich persönlich, sie nicht mehr.
Als Student, ich habe 1971 zu studieren begonnen.
Alle zwei Jahre gab es einen schnelleren Rechner.
Der war noch dazu billiger.
Es hat sich überhaupt nicht gelohnt, parallel zu rechnen, weil es
erstens schwierig war und zweitens der Rechner viel schneller wurde,
als ich die komplizierten Programme schreiben konnte.
Nur diese Murschi-Regel oder das Murschi-Gesetz hat aufgehört zu
gelten.
Das war also so, dass sich die Anzahl der Transistoren alle zwei Jahre
verdoppelt.
Gleichzeitig ist auch die Taktfrequenz angestiegen, aber auch dieser
Anstieg wird übrigens nicht durch die Murschi-Regel betroffen, sondern
nur die Anzahl der Transistoren.
Jetzt gibt es also dieses Bild hier, das geht bis 2015, es gibt auch
ein neueres, das müssen wir noch besorgen.
Es gibt also folgende Kurven.
Schauen wir uns erst die blaue an.
Nein, schauen wir uns diese grünliche an, um es am schlechtesten zu
sehen.
Das ist die Taktfrequenz in Megahertz.
Sie sehen, die beginnt hier zu stagnieren, und zwar so um 2005 rum.
Wenn man eine Kurve reinlegt.
Und das ist bei hoch 3 in Megahertz.
Megahertz hoch 3 sind Gigahertz.
Und es liegt so etwa bei 4.
Wenn Sie sich hier diese logarithmische Skala vorstellen, geht immer
in Potenzen von 10.
10 hoch 4, 5, 6, also hier 10 hoch 3.
Mal Megahertz gibt es Gigahertz.
Also so um die 3-4 Gigahertz stagniert die Taktfrequenz.
Wir werden nicht mehr schneller.
Und der Stromverbrauch stagniert auch, weil wenn man die Taktfrequenz
jetzt noch beschleunigen würde, dann werden wir natürlich, wenn Sie
hier so denken, es würde linear weitergehen, werden wir hier bei 10
oder 100 Gigahertz dann werden die Chips so heiß, dass sie einfach
kaputt gehen.
Also sind wir hier an einer Grenze.
Aber Sie sehen diese überragende rote Kurve, die ist noch anscheinend
ungebrochen, obwohl das auch enden wird.
Das ist die Anzahl Transistoren pro Flächeneinheit.
Da sind wir also jetzt in Tausenden.
Bei 10 hoch 3, das sind also Millionen.
Das sind 10 Millionen, 100 Millionen, 1 Milliarde.
Wir sind bei fast 10 Milliarden Transistoren pro Chip.
Durch immer weitere Verkleinerung bei gleichbleibender
Verbrauchsleistung.
Was wollen wir mit diesen vielen Transistoren?
Man hat das in der Vergangenheit dazu benutzt, die Prozessoren
schlauer zu machen, sodass sie schneller liefen.
Zum Beispiel mit Pipeline-Verarbeitung, gleichzeitige Verarbeitung.
Und heute machen wir das hier, das ist nämlich die Anzahl der
Prozessorkerne.
Heute verdoppelt man einfach nur noch die Anzahl der Prozessorkerne.
Und das liegt jetzt im zweistelligen Bereich, also 10 bis 100 Stück
auf einem Transistor, auf einem Chip.
Obwohl das auch nicht ganz richtig ist.
GPUs haben Tausende.
Tausende von Prozessoren auf einem Chip.
Tausende!
Wir sind allerdings keine Universalprozessoren.
Die laufen eher wie ein Vektorprozessor.
Es gab ja also so einen Knick in der Taktfrequenz.
Der war etwa 2005.
Das ist ein älteres Diagramm.
Wie sieht so ein Doppelprozessor oder Mehrfachprozessor aus?
Sie haben ein Chip.
Das ist übrigens der Intel Core Duo.
Das ist also einfach ein Pentium-Prozessor, zwei Stück davon, mit
einem Bus verknüpft.
Und auch das L2-Cache passt gleich drauf.
Und das L1-Cache, das am nächsten am Kern ist, ist auch drauf.
Das gibt es schon seit...
ich weiß nicht mehr...
Das gibt es seit mindestens 15 Jahren.
Und dann gab es andere, die sagten, wir haben zwei, wir haben vier.
Der Intel Quad Core kam dann heraus.
Der hatte vier.
Der hatte dann gemeinsame L2-Caches hier und einen Memory-Controller
und dann zum Hauptspeicher zu den Sockeln mit den DRAM-Chips.
Müssen wir jetzt nicht so genau wissen.
Es kommt darauf an, wie diese Caches konsistent gehalten werden.
Das ist übrigens ein ganz, ganz komplexes Problem.
Aber wir kümmern uns weiter nicht drum.
Dann kam ein San Niagara 2 raus, der hatte gleich acht Prozessoren auf
einem Chip.
Das kann man übrigens hier sehr gut sehen.
Hier sind sie.
Acht Stück.
Und Sie sehen, das nimmt nicht mal die Hälfte des Chips weg.
Hier in der Mitte ist ein sogenannter Kreuzschienenverteiler, der dazu
sorgt, dass sich die untereinander verständigen können.
Dann sind noch die einzelnen Caches mit dabei.
Angezeigt sind die L2-Caches.
Die sind pro Prozessor.
Und der Rest interessiert uns jetzt nicht so genau.
Ja.
Das war 2007 oder so.
2005.
Erste Version 2005.
Das ist also schon 15 Jahre fast her.
Und hatte noch zusätzlich acht Hardwarefäden pro Prozessor.
Hardwarefäden heißt, sie haben komplette Registersätze.
Also, acht Stück kompletter Registersätze für acht Fäden.
Wozu brauche ich das?
Das brauche ich für jeden Kern für sich, für jeden Prozessorkern für
sich, heißt es, er kann gleichzeitig acht Fäden am Laufen halten,
wobei er zwischen den Fäden sehr schnell schalten kann.
Ich kann also sagen, wenn Faden 1 ein Datum braucht, das nicht im
Cache ist, das heißt, es wird jetzt 500 oder 1000 Zyklen dauern, bis
dieses Datum aus dem Cache geholt ist.
So langsam ist ein Hauptspeicher.
Jetzt schalte ich auf den Zweier, wenn der fertig ist zu laufen.
Das heißt also, was jeder dieser Prozessoren macht, er hat die
kompletten Registersätze für acht lauffähige Programme vorliegen und
die warten nur auf die Ausführung und wenn irgendeiner aus irgendeinem
Grund stoppen muss, zum Beispiel um Daten zu holen, ich schalte auf
den Nächsten, lasse den laufen, bis der hängt, dann laufe ich den
Nächsten und dann den Nächsten und dann den Nächsten und dann fange
ich von vorne an.
Also, der kann in jedem Instruktionszyklus den Prozess, den Faden
wechseln.
Okay.
2011 kam Intel mit etwas heraus, das auch Leute aufgerüttelt hat.
Das war nämlich so, das hat jetzt nur vier Pentiums drauf, aber gleich
das ganze L3-Cache mit dabei, die L2-Cache natürlich auch nicht, aber
noch zusätzlich Grafikprozessoren.
Das, was heute separat noch gehandelt wird als GPU, sagen wir in
Zukunft, macht das alles ein Chip.
Das kann sein, dass uns das passieren wird.
2011 waren das vier, sechs grafische Einheiten, demnächst acht, zwölf
Ausführungseinheiten.
Für uns Programmierer ist das nicht unbedingt ein schönes Geschenk,
weil wir müssen jetzt sagen, wir haben unterschiedliche Befehlssätze
auf einem Chip.
Die hier laufen mit anderen Befehlssätzen als die hier.
Wir müssen die Arbeit irgendwie aufteilen.
Wie wechseln wir das um?
Wann?
Wie?
Warum?
Wie oft?
Und wie schreiben wir die Software dafür?
Wir müssen jetzt anfangen, uns ein bisschen darum zu kümmern.
Dieses Problem werden wir hier nicht behandeln, aber
noch ein Detail, Sie können diese Dinger übertakten.
Das heißt, Sie können sie kurzfristig, das heißt für eine halbe
Minute, ich weiß nicht mehr genau, 10 oder 15 Prozent schneller
takten.
Also statt 3 GHz mit 3,5 oder sowas.
Das bedeutet aber, dass dieser Prozessor dann sehr heiß wird.
Deswegen nur kurzzeitig.
Und dann müssen Sie ihn wieder abschalten.
Mit einem anderen Rechnen.
Das mache ich dann, wenn ich kurzzeitig einen Schub an Leistung
brauche, auf Vollgas drücken will, und dann wird das Chip aber so
heiß, dass ich das nächste Ding erstmal ruhen lassen muss, dass es
sich wieder abkühlt.
Das müssen wir jetzt programmieren, um es zu nutzen.
Ok, Grafikprozessoren, der hatte 2010 352 Kerne.
Müssen wir nicht alles anschauen.
Die Mobiltelefone waren natürlich auch nicht weit hinterher.
Zum Beispiel ein 5-Kern-Mobiltelefon, gab es schon 2012.
Das sieht also so aus, man kann gut diese 5 Kerne sehen.
Da gibt es einen Kern, der läuft mit 500 MHz.
Das ist der Batteriesparer.
Der macht eigentlich alles.
Nur wenn sie dann plötzlich ein Spiel machen wollen, oder sonst
irgendetwas Aufwendiges zu rechnen haben, dann springen diese Kerne
mit ein, die laufen dann 3x so schnell und davon gibt es 4.
Natürlich wollen sie die nicht immer anhaben, denn das saugt ihnen die
Batterie ziemlich schnell leer.
Und außerdem gibt es noch eine GPU, und es gibt noch einen
Videocodierer und Decodierer für Musik ebenfalls.
Also das alles Zeugs hier rum sind auch noch Prozessoren.
Alles auf einem Chip, in ihrer Hosentasche oder Handtasche.
Also wir haben jetzt eine neue Murschi-Regel, das sagt, die
Verdopplung der Anzahl der Prozessoren pro Chip mit jeder Chip
-Generation.
Wobei die Taktfrequenz etwa gleich bleibt.
Die kann sogar ein bisschen sinken.
Okay.
Das sind die Folgen.
Alle Hersteller machen inzwischen Parellrechner.
Sie haben sie alle.
Seit 2005 werden die Server damit ausgerüstet.
Und seit 2010 die Mobiltelefone, und dazwischen kamen die Laptops.
Und natürlich auch die eingebittenen Anwendungen,
Steuerungsanwendungen in Autos zum Beispiel, sind heute alles
Multikernrechner.
Sie werden damit konfrontiert werden.
Darum gehe ich her und sage Ihnen, wie man die Dinger programmiert.
So.
Ich mache also jetzt Parellprogrammier-Abbildung schon seit 10 Jahren.
Im zweiten Semester.
Das heißt Ihnen.
Und damit fangen wir jetzt an.
Ich meine, dass die Parellprogrammierung zu einer Job-Garantie für Sie
werden kann.
Wenn Sie wissen, wie das geht, dann sagen Sie, oh Gott, den brauchen
wir.
Insbesondere, wo jetzt auch noch die KI dazukommt.
Die Lernverfahren, die müssen auch alle parallel laufen.
Die laufen alle parallel.
Da brauchen wir jemanden, der das versteht.
Das sind Sie.
Da gibt es natürlich noch viel mehr Vorlesungen dazu.
Aber wir machen jetzt mal die Grundlagen.
Was heißt also...
9 Minuten haben wir noch.
Was heißt jetzt Parallel?
Das ist aus dem griechischen Parallelos.
Allgemein nebeneinander laufend.
Im gleichen Abstand.
Im gleichen Abstand denken Sie an Schienen.
Oder an parallele Geraden.
Das kennen Sie ja auch.
Aus der Geometrie.
In der Informatik bedeutet das einfach gleichzeitig ablaufend.
Simultan.
Echt simultan.
Manchmal hören Sie auch das Wort nebenläufig und parallel.
Nebenläufig heißt im Englischen concurrent.
Das heißt nicht kausal voneinander abhängig.
Die machen zwei gleichzeitig irgendetwas, aber sie haben keinerlei
Interaktion.
Das nennt man nebenläufig.
Gewissermaßen nebeneinander her.
Ohne voneinander zu wissen.
Und parallel bedeutet wirklich echt gleichzeitig.
Diesen feinen Unterschied brauchen wir gar nicht zu machen.
Ich spreche immer von parallel und das heißt einfach gleichzeitig.
Dann gibt es zwei wichtige Architekturunterscheidungen.
Und die haben mit dem Speicher zu tun.
Gibt es einen gemeinsamen Speicher oder einen verteilten Speicher?
Im gemeinsamen Speicher haben Sie einen riesigen Speicher und 10.000
Prozessoren die schreiben den Speicher und lesen aus dem Speicher.
Alle gleichzeitig.
Es gibt keinen Unterschied, keiner hat eine spezielle Speichereinheit
oder sowas.
Jeder liest und schreibt den globalen Speicher.
Es ist übrigens kompliziert zu verwirklichen.
Einen so großen Speicher, der vielfach parallelen Zugriff erlaubt, ist
schwierig zu realisieren.
Wie der Mach sei, bis zu einem bestimmten Maße können wir das
erreichen?
Dann gibt es den verteilten Speicher.
Da sagen wir, jeder Prozessor hat seinen eigenen Speicher.
Wenn Sie also heute Ihre Rechner nehmen würden und zu einem Netz
zusammenschalten würden und darauf dann eine Anwendung laufen ließen,
die auf allen Rechnern gleichzeitig Arbeit leistet, dann wäre das ein
Rechner mit verteiltem Speicher.
Jeder Rechner hat seinen eigenen Speicher.
Der andere kann nicht auf ihren Speicher zugreifen, sondern muss erst
eine Botschaft schicken, die sagt, gib mir mal die Daten aus deinem
Speicher von da bis da in der und der Größe und schick mir das als
eine Botschaft zurück, damit ich mit diesen Daten dann weiter arbeite.
Das kann zum Beispiel ein Zwischenergebnis sein, das geliefert werden
muss an den anderen.
Aber Sie sehen schon, das geht nicht ohne Netz und nicht ohne
Botschaftenaustausch.
Wir werden uns hauptsächlich mit gemeinsamen Speicher befassen, weil
das zunächst mal am einfachsten zu programmieren ist.
Jetzt brauchen wir einfach ein Modell.
Wie soll ich das jetzt alles verstehen auf eine Reihe kriegen?
Wir brauchen ein Modell, das jetzt mit Java einigermaßen zusammenhängt
und da gibt es jetzt diese beiden Begriffe Prozess und Faden oder
Kontrollfaden, im englischen Process und Thread.
Und diese Prozesse enthalten in der Regel mindestens einen Faden, aber
möglicherweise mehrere.
Und diese Fäden, die in einem Prozess sitzen, haben einen gemeinsamen
Speicher.
Was der gemeinsame Speicher enthält, das zeige ich Ihnen gleich auf
der nächsten Folie, aber erst noch schauen wir uns nochmal das durch.
Also vom Betriebssystem erzeugt, Sie sagen, ich brauche jetzt einen
Prozess.
Dann bekommen Sie all das Zeug, das Sie dazu brauchen, mit den
Dateischnittstellen, mit den Netzwerkschnittstellen und ein paar Fäden
und den Hauptspeicher.
Ja.
Da gibt es zum Beispiel ein Codesegment, da steht der Code drin für
diese Fäden, die Sie ausführen, die Programminstruktionen.
Dann gibt es ein Datensegment, da stehen die Daten, zum Beispiel für
die Halde, für den Keller, für globale Variablen, die es gibt und
mindestens einen Kontrollfaden.
Ja.
Wenn Sie umschalten müssen von einem Prozess mit seinen Kontrollfäden
auf einen anderen, da dauert es etwas länger, weil alle
Registerinhalte invalidiert werden müssen und neu beschrieben werden
müssen, wie ich vorhin gerade erzählt hatte.
Wenn Sie von einem Faden auf den nächsten umschalten müssen, der zu
einem Prozess gehört, dann geht das rasch.
Und im Idealfall haben Sie gleich einen Parallelprozessor, der sowieso
die verschiedenen Kontrollfäden auf verschiedenen Prozessoren hält und
die dann sowieso echt parallel laufen können.
So.
Ein Faden hat jetzt seinen eigenen Befehlszeiger.
Der Befehlszeiger sagt, was ist mein nächster Befehl?
Der wird ausgeführt und dann wird der Befehlszeiger eins
weitergeschaltet, wird der nächste ausgeführt, weitergeschaltet,
weiter, weiter, weiter, weiter, weiter, weiter, möglicherweise mit
einem Sprung zurück an den Anfang, dann haben Sie eine Schleife.
Der hat einen Keller.
Im Keller werden die Methodenaufrufe gestapelt, ein nach dem anderen
und wir kehren dann zurück.
Das heißt, der Keller wächst und schrumpft und wächst und schrumpft.
Das nennt man Keller oder Stack und dann gibt es die Register.
Das sind die lokalen Register, die man als Programmierer in Java gar
nicht zu Gesicht bekommt, aber wir brauchen die natürlich auch.
Die gehören auch dazu.
So.
Und jeder Faden eines Prozesses hat jetzt einen gemeinsamen
Adressraum.
Das heißt, Sie sehen einen gemeinsamen Speicher, so wie ich es vorhin
gesagt habe.
Keine verteilten Speicher, sondern jeder Prozess bietet allen seinen
Fäden einen großen Speicher, mehrere Gigabyte.
Da sitzen drin die Koden und Datensegmente und andere Ressourcen und
Informationen über zum Beispiel geöffnete Dateien, geöffnete
Netzwerkverbindungen.
Da gehört ja immer mordsmäßig Daten dazu oder die Grafiksachen und so
weiter.
So.
Jetzt schauen wir uns ein Bild dafür an.
Hier haben wir einen Prozess.
Der Prozess enthält Daten für die einzelnen Fäden, enthält den Code,
daran stecken die Anweisungen und die Halde.
Auf der Halde liegen die Objekte.
Ich sage da immer Halde dazu, weil ich das so gelernt habe.
Das heißt nicht Haufen oder so.
Leute nicht wissen, wie man Heap übersetzt.
Heap ist die Halde.
Das ist eigentlich ein tolles Wort, weil Halde sagt man eigentlich,
ich kann da immer was draufstapeln und am Hinten fällt es dann runter.
Und der Garbage Collector, das ist natürlich der Speicherbereiniger,
der sammelt die runtergefallenen Objekte wieder auf.
Das ist die Halde, da werden die Objekte gespeichert und natürlich
wieder eingesammelt, wenn sie verbraucht sind und nicht mehr benötigt
sind, so muss ich sagen.
Die Anweisungen sind klar, das sind also einzelne Instruktionen für
die Maschine selbst.
Aber was macht dieses Zeug hier?
Schauen wir uns das mal genauer an.
Ein solcher Faden hat all dieses Zeug drin.
Das ist ein Hauptspeicherbereich.
Ich spreche jetzt nicht von Maschinenregistern, sondern von
Speicherinhalten.
Und zwar brauche ich dafür einen Programmzähler, den muss ich mir
speichern können, wenn zum Beispiel ein Faden deaktiviert wird.
Selbstverständlich ist der Programmzähler dann später in einem
Register des Rechners.
Der Programmzähler.
Hier ist die nächste Instruktion, die auszuführen ist.
Wenn sie ausgeführt ist, dann schalte ich den Programmzähler 1 weiter.
Das ist die nächste Instruktion.
Das ist eine ganz normale Instruktionsausführung.
Der Kellerzeiger.
Stackpointer.
Der sagt, was das letzte belegte Element des Kellers ist.
Der ist nämlich hier.
Auf dem Keller liegen die sogenannten Aktivierungen.
Oder englisch Activation Record.
Die Aktivierung oder Activation Record ist der Inhalt, den ich
brauche, um eine Methode aufzurufen.
Insbesondere, ich zeige Ihnen gleich, was da drück ist, da gehört zum
Beispiel die Rücksprungadresse dazu.
Aber auch Informationen über den Keller.
Haben wir also den hier.
Der Aktivierungszeiger sagt mir natürlich, was die augenblickaktive
Aktivierung der augenblicklaufenden Methode ist.
Hier geht es los.
Hier liegen schon mal Daten, Zwischenergebnisse, lokale Variablen
dieses Methode.
Wenn irgendwelche komplexe Ausdrücke ausgerechnet werden müssen, mit
25 Operatoren, dann werden die Zwischenergebnisse einfach nach dem
anderen aufgestapelt.
Und wenn das zum Ergebnis zusammenschrumpft, dann schrumpft auch der
Keller wieder zu einem Wert.
Und der Rest ist frei.
Und dann kann es natürlich sein, dass ich ein blödes Programm
schreibe, wie zum Beispiel das wir vorhin gerade gehabt hatten, beim
Assoziationsaufbau, die dann immer wieder rekursioniert, dann läuft
mir irgendwann mal der Keller über und dann hat man den berühmten
Stack Overflow.
Das ist der Überlauf des Kellers.
Also, was brauchen wir noch?
Programmzähler haben wir, Aktivierungszähler haben wir, Zeiger haben
wir, Kellerzeiger haben wir und dann gibt es noch die übrigen
Register.
Wie gesagt, können wir nicht direkt programmieren, brauchen wir also
nicht viel darüber zu wissen, außer dass sie existieren und natürlich
für jeden Faden gespeichert werden müssen.
Das ist gewissermaßen der Inhalt, den ich brauche, um einen Faden
weiter drauf zu lassen oder zu starten.
Jetzt müssen wir noch die Aktivierung anschauen.
Das ist das Letzte, was wir machen und dann sind wir für heute fertig.
So eine Aktivierung, was steckt da alles drin?
Also Sie müssen verstehen, die Aktivierung oder Activation Record wird
angelegt, wenn eine Methode aufgerufen wird.
Dann wird so ein Block Daten angelegt.
Und da ist also drin zum Beispiel Platz für den Rückgabewert.
Wenn das eine Funktion ist, die einen Rückgabewert bilden soll, dann
wird hier vorgesehen, dass hier oben noch so ein Schlitz übrig ist.
Da kommt das Ergebnis rein.
Zum Beispiel der C-Zustand.
Wenn dann die Aktivierung abgebaut wird, dann wird sie komplett
abgebaut bis auf den Letzten.
Der gehört jetzt zu dem vorläufigen Aktivieren.
Ach ja, genau, das Ergebnis wollte ich ja.
Das liegt jetzt oben auf dem Keller.
Auf meinem Keller.
Und ich kann es einfach abgreifen, speichern und eine andere Variable
speichern.
Zum Beispiel.
Dann die Rücksprungadresse.
Die brauche ich natürlich, wenn ich mit der Aktivierung fertig bin.
Bin ich hier irgendwo im Code durchgelaufen, bin jetzt am Ende der
Aktivierung und es kommt ein Return.
Das sagt also zurückspringen an die Stelle, wo du aufgerufen wurdest.
Und diese Adresse steht auch hier.
Dann gibt es noch ein Zeiger auf vorige Aktivierung.
Ich muss ja den Aktivierungszeiger beim Rückkehr nicht nur an die
richtige Stelle zurückspringen im Code.
Ich muss auch an die richtige Stelle zurückspringen vor der vorigen
Aktivierung.
Den Aktivierungszeiger wieder hinsetzen.
Und den Aktivierungszeiger brauche ich, um lokale Variablen zu
adressieren.
Jede lokale Variable wird adressiert in der übersetzten Form, nicht
als A, B, C oder so etwas, sondern als Abstand vom Beginn der
Aktivierung.
Also als eine relative Adresse.
Und wenn ich den Aktivierungszeiger jetzt nicht retten bzw.
zurücksetzen würde auf die vorige bei der Rückkehr einer Methode, dann
wüsste ich nicht, wo meine lokalen Variablen sitzen.
Und dann kommen die Parameter.
Aha, die Parameter brauchen hier auch noch.
Die müssen beim Aufruf übergeben werden.
Die werden auf den Keller gelegt.
Und dann Platz für die lokalen Variablen.
Und Sie sehen schon, die lokalen Variablen haben jetzt einen Abstand
von hier.
Den setzt der Compiler fest, der die Dinge anlegt.
Und jeder Zugriff übersetzt in einen indirekten Zugriff auf
Aktivierungszeiger plus die Distanz.
So, jetzt haben Sie also einen Schnelldurchgang gesehen.
Wie eigentlich ein Java-Programm runtergebrochen wird auf eine
Maschine, die nichts versteht von Objektorientierung.
Polymorphie hatten wir an einer anderen Stelle erklärt.
Und jetzt wissen Sie, was der Aufrufskeller macht.
Und das Einzige, was Sie jetzt noch demnächst hinzulernen müssen, ist,
was passiert, wenn wir mehrere solcher Fäden haben?
Ich gehe also wieder auf dieses Bild hier zurück.
Mehrere solcher Fäden sind, die gleichzeitig arbeiten.
Das heißt, deren Codezeiger läuft hier durch die Anweisung durch mit
einer sagen wir mal atemberaubenden Geschwindigkeit von Gigahertz.
Und gelegentlich ist es dummerweise so, dass mehrere Fäden auf die
gleiche Variable zugreifen wollen, sie gleichzeitig ändern.
Und dann wissen wir nicht, wer recht hat, wer das Ergebnis sein soll.
Und diese Inkonsistenzen, die dabei auftreten können, zeige ich Ihnen
nächstes Mal und wie wir sie verhindern.
Dankeschön!