 Ja, herzlich willkommen zur Veranstaltung Programmierparadigmen. Heute geht es um Concurrency, also Nebenläufigkeit, also im Grunde der Frage, wie kann man in einer Programmiersprache ausdrücken, dass mehrere Sachen parallel zueinander ausgeführt werden sollen und wie kann man sicherstellen, dass während dieser Ausführung trotzdem alles so geschieht, wie der Programmierer das möchte und insbesondere, dass bestimmte Dinge miteinander synchronisiert werden. Bevor wir so richtig ins Thema einsteigen, fangen wir mit einer kleinen Aufhermübung an, und zwar in Form eines Stückes Java Code, bei dem die Frage ist, was macht der Code, also was gibt dieses Stück Java Code schlussendlich aus. Und in diesem Stück Code werden verschiedene Programmiersprachen, Konstrukte verwendet, die erlauben, parallele Ausführungen auszudrücken und wir werden die dann im Laufe der Veranstaltung noch ein bisschen genauer kennenlernen, jetzt erstmal zum Aufhören einfach die Frage, was macht denn dieser Code. Und wie immer bei dem Quiz ist, würde ich Sie bitten, an der Stelle das Video kurz anzuhalten, dann dem Ilias darüber abzustimmen, was hier rauskommt und anschließend dann weiterzuschauen. Schauen wir mal die Lösung an, also was wir hier haben, ist zum einen diese Main-Methode, die dann als erstes ausgeführt wird und in dieser Main-Methode haben wir einen Aufruf dieser anderen Methode namens aRaceFlag und dieser Aufruf findet statt über diese Execute-Methode, die werden wir noch ein bisschen genauer anschauen, aber was die im Grunde macht, ist ein parallelem Thread zu starten, in dem dann RaceFlag ausgeführt wird. So, was macht dieses RaceFlag? Es schreibt in dieses Feld namens Flag, dass ursprünglich false ist und wenn RaceFlag dann ausgeführt wird, auf true gesetzt wird und hier unten, also in dem Code der Main-Methode, der dann parallel zur Auswirkung von RaceFlag stattfindet, lesen wir dieses Flag-Field zweimal, nämlich einmal um festzustellen, wann wir aus dieser Wildschleife wieder rauskommen und dann schlussendlich, um das Flag auszugeben. So, jetzt könnte man erwarten, dass wir aus dieser Wildschleife ja erst rauskommen, wenn Flag dann tatsächlich auf true gesetzt wird und deswegen dann true ausgegeben wird. Leider ist das aber eben nicht so, warum genau werden wir im Laufe dieses Vorlesungsblockes noch genauer anschauen, aber ganz einfach gesagt ist der Grund, dass es keine Synchronisierung zwischen diesen beiden Threads gibt, also dem Thread, in dem RaceFlag ausgeführt wird und dem anderen Thread, in dem die Main-Methode läuft und das bedeutet, dass jeder beliebige Wert den Flag irgendwann mal hatte, hier gelesen werden kann, das heißt, Flag kann sowohl false oder true sein, eventuell auch erst true sein und später wieder false sein, auch wenn das in der eigentlichen Auswirkung nicht stattfindet, ganz einfach, weil die beiden Threads nicht miteinander synchronisiert sind und das Java-Memory-Model, was wir noch genauer kennenlernen werden, das so vorgibt. Und was bedeutet das jetzt für den Code? Was kommt hier raus? Naja, es gibt drei Möglichkeiten. Entweder der Code hängt ewig in dieser Wildschleife, weil er immer nur false liest, das kann durchaus passieren, oder er gibt True Haus, also das, was man vielleicht so intuitiv erwarten würde, oder er gibt eben auch false aus, was eben genau dann passieren kann, wenn hier oben einmal true gelesen wird und wir somit aus der Schleife rauskommen, hier unten dann aber ein älterer Wert gelesen wird, nämlich false und der dann schlussendlich ausgegeben wird. So, wenn das Beispiel jetzt vielleicht ein bisschen verwirrend war, dann keine Angst, wir kommen darauf noch zurück und schauen uns natürlich an, was da eigentlich so alles dahinter steckt und zwar indem wir diese vier Themenblocke hier durcharbeiten. Also jetzt hier in dem Video gibt es einfach mal eine kleine Einführung, so ein bisschen die Thermologie und uns ein paar Grundprobleme, die es bei Concurrency eigentlich immer gibt. Dann schauen wir uns so ein paar grundlegende Konzepte an, die unabhängig von bestimmten Sprachen einfach sehr relevant sind, schauen uns dann an wie Synchronisierung, also die Tatsache, dass wir zwischen parallelen Ausführungssträngen gelegentlich einen gewissen Austausch haben wollen und eben feststellen wollen, dass die an derselben Stelle sind oder dass das, was in dem einen Ausführungsstrang geschrieben wird, in dem anderen auch gelesen wird, wie man das implementieren kann, also wie man Synchronisierung implementieren kann und dann im vierten Teil schauen wir uns ein paar konkrete Sprachbeispiele an, wie all diese Sachen implementiert werden und spätestens dann sollte auch das Beispiel von gerade eben hoffentlich klar werden. Vielleicht fangen wir erstmal mit der grundsätzlichen Frage an, warum wir uns überhaupt mit Concurrency oder Nebenläufe beschäftigen. Man könnte auch sagen, naja, ich kann das pro Korn ganz einfach so schreiben, dass einfach eine Sache nach der anderen passiert und dann habe ich all diese Probleme, die wir jetzt hier sehen werden, nicht und brauche auch diese komplexen Sprachkonstrukte nicht und das stimmt, in manchen Programmen ist das auch der beste Weg, es gibt allerdings drei Gründe, warum man sich in der Praxis dann eben doch häufig mit Concurrency herumschlagen muss. Das eine ist, dass manchmal die logische Struktur des Problems um das es eigentlich geht verlangt, dass man das Problem durch Parallelismus oder Nebenläufigkeit angeht. Also wenn wir uns zum Beispiel anschauen, was ein Server macht, der gleichzeitig mehrere Clients bedienen soll, dann ist das ein inherent, nebenläufiges Problem, denn diese Clients warten nicht brav und kommen einer nach dem anderen, sondern die kommen halt gleichzeitig, sprich, der Server muss einfach Concurrency benutzen, um diese Clients tatsächlich zeitnah bedienen zu können. Der zweite Grund, warum Concurrency sehr wichtig ist und das ist insbesondere etwas, was seit ungefähr 2005 wichtiger und wichtiger geworden ist und mittlerweile auf jedem Laptop oder sogar Handy relevant ist und das ist, dass wir parallele Hardware haben, die man ausnutzen sollte, wenn man tatsächlich den entsprechenden Computer, um den es gerade geht, richtig nutzen möchte. Also bis 2005 lief das im Prinzip so, dass die CPUs immer schneller und schneller wurden und sich niemand wirklich um Parallelität kümmern musste, bloß um seine Programme schneller zu machen. Seit 2005 hat sich das aber geändert und wir haben seitdem hauptsächlich Multicore Prozessoren in zum Beispiel Laptops drinstecken und um die wirklich nutzen zu können, muss man das Programm parallelisieren und dann hat man eben all die Concurrency Probleme, um die es hier geht. Der dritte Grund ist, dass komplexe Software oft eben nicht nur auf einem Computer läuft, sondern auf physisch verteiltem Computer läuft. Das heißt, wir haben vielleicht auf einem Kontinent eine Reihe von Rechnern, die in irgendeinem Data Center stehen und auf einem anderen Kontinent auch wieder und die müssen natürlich auch alle miteinander synchronisiert werden. Und genau da haben wir auch wieder diese ganzen Concurrency Probleme. Zweitens ein bisschen was zur Terminologie. Also es gibt drei Begriffe, die so ein bisschen das Gleiche beschreiben, aber doch eigentlich nicht genau das Gleiche sind, nämlich Concurrent, Parallel und Distributed. Also Concurrent oder auf Deutsch nebenläufig bedeutet, wir haben zwei oder mehrere Aufgaben oder Tasks, die in ihrer Ausführung an irgendeinem Punkt sind und wir aber nicht genau sagen können, an welchem Punkt die jetzt relativ zueinander immer sind. Also wir wissen zum Beispiel nicht, dass die eine Aufgabe schon am Ende ist, während die andere noch in der Mitte sein muss, sondern es könnte auch sein, dass es eben genau anders herum ist. Parallelität bedeutet, dass wir auch wieder zwei oder mehr solche Tasks haben, die aber aktiv zur selben Zeit laufen. Das heißt, das ist sozusagen ein Schritt mehr, denn jetzt sagen wir nicht nur, dass wir irgendwo in der Ausführung dieser Tasks sind, sondern die laufen tatsächlich zur gleichen Zeit. Und in der Praxis bedeutet das, dass wir zumindest mehrere Chors auf dem Prozessor brauchen oder vielleicht auch auf mehreren Maschinen das Ganze am Laufen haben. Distributed, das ist dann so der letzte Schritt, bedeutet eben, dass das Ganze nicht nur auf einem Computer funktioniert, sondern auf physisch separaten Prozessoren. Und das ist sozusagen der, das umfasst die anderen beiden Begriffe, also im Prinzip bauen die so aufeinander auf und Distributed ist also parallel und außerdem noch auf verschiedenen Computern. Wenn wir jetzt mal nicht nur die Programmiersprache anschauen, sondern so den Computer als Ganzes, dann gibt es Parallelität eigentlich auf mehreren Ebenen. Also das eine ist, das was hier oben als erstes steht, nämlich dass innerhalb der Schaltkreise, die in so einem Computer stattfinden, extrem viel Parallelität stattfindet, denn die Signale werden da gleichzeitig auf verschiedenen Schaltkreisen propagiert. Und ja, das kriegt man in der Regel als Programmiere gar nicht mit, aber da passieren viele Dinge parallel in der Hardware. Die zweite Art von Parallelität, die auch von der Hardware Implicit behandelt wird, ist Instruction Level Parallelism. Und das bedeutet, dass die CPU eben nicht nur eine Instruktion brav nach der anderen abarbeitet, sondern die teilweise parallelisiert, ganz einfach, weil es dadurch insgesamt schneller geht. Also zum Beispiel braucht das Laden eines Wertes aus dem Hauptspeicher relativ lange und was eine cleveres CPU dann macht, ist, dass die Instruktion so miteinander verschafft wird, bzw. parallel zueinander ausgeführt werden, dass zum Beispiel ein Wert mal geladen wird für die Instruktion, die dann erst in fünf Schritten kommt. Und in der Zwischenzeit wird dann irgendeine Berechnung ausgeführt mit Werten, die wir vorher schon geladen haben. Das heißt, die Sachen, die einzelne Instruktion, die man vielleicht so im Assembler sieht, finden ja nicht genau nacheinander statt, sondern sind eigentlich so ein bisschen überlappt. All das passiert aber Implicit in der Hardware. Ist auch total spannendes Thema, aber ist nicht wirklich Thema diese Veranstaltung. Das heißt, hier interessieren wir uns dafür im Prinzip nicht, sondern gehen davon aus, dass das für uns transparent in irgendeiner Art geschieht. Die unteren beiden Arten von Parallelismus oder Level von Parallelismus sind dann relevanter aus Sicht der Programmiersprache. Und zwar ist das zum einen der Vector Parallelism, wo es darum geht, dass bestimmte Arten von Prozessoren, insbesondere zum Beispiel GPUs, eben nicht nur eine Instruktion auf einem Stück Datum ausführen, sondern diese eine Instruktion auf einem ganzen Vector von Daten ausführen können. Und damit diese eine Instruktion dann natürlich parallel ausführen, und zwar auf eine Art und Weise, die genau dasselbe aber eben auf verschiedenen Daten macht. Und dann das vierte Level von Parallelismus, und das ist das worum es hauptsächlich hier in dieser Vorlesung gehen soll, ist Thread Level Parallelism. Also der Form von Parallelismus, wo unabhängig oder ja verschiedene Threads, die zumindest potenziell was anderes machen können, parallel ausgeführt werden. Wenn man jetzt nebenläufig oder parallel ausführung hat, dann können da gewisse Dinge bei schief gehen. Wir werden uns das noch genauer anschauen, aber bloß immer schon mal ein bisschen gefühlt dafür zu geben, habe ich jetzt hier mal zwei Beispiele. Und zwar als erstes ein Beispiel von Parallel oder nebenläufig ausgeführten Tasks, die aber unabhängig voneinander sind. Was wir hier sehen, ist ein Stück Code, was die Task Parallel Library in C sharp benutzt. Diese Bibliothek erlaubt es bestimmte Aufgaben parallel zueinander auszuführen, und zwar mit einer relativ einfachen Syntax. Was wir hier haben, ist so ein Aufruf von diesen Parallel.for. Und was das im Grunde macht, ist, das hat hier so eine Funktion, die hier losgeht und bis hier unten geht. Und diese Funktion wird für jedes i zwischen 0 und 100 einmal ausgeführt und macht dann das, was hier in dem Buddy der Funktion steht. Und was hier konkret passiert, ist, dass wir einen Array namens a haben und das i-Te-Element dann jeweils aktualisieren, indem wir diese Funktion fo darauf aufrufen. Das heißt, diese Funktion fo schaut sich jedes Element unseres Arrays unabhängig voneinander an. Es gibt keine Abhängigkeiten zwischen den einzelnen Tasks, die hier ausgeführt werden. Und deshalb brauchen wir in der Stelle auch keine Synchronisierung zwischen diesen Tasks, sondern die können wirklich nebeneinander ausgeführt werden. Und das bedeutet auch, dass während der Ausführung dann alle Cores, die auf der CPU zur Verfügung stehen, benutzen kann. Also in dem Fall bis zu 100, denn wir hätten ja hier dann 100 verschiedene Aufgaben. Wenn die Tasks unabhängig voneinander sind, wie in dem Beispiel gerade eben, ist das im Prinzip der einfache Fall etwas spannender wird, wenn Abhängigkeiten zwischen den Tasks bestehen. Wenn wir jetzt nochmal das Beispiel von gerade eben anschauen, also diese Funktion fo, die auf jedem Element des Arrays aufgerufen wird, aber jetzt davon ausgehen, dass diese Funktion so definiert ist, wie man hier sieht, dann besteht plötzlich eine Abhängigkeit zwischen diesen Tasks. Der Grund ist, dass diese Funktion eben nicht nur irgendeine Berechnung auf dem Wert N, der in dem Array steht, vornimmt, sondern außerdem noch mitzählt, wie oft bei dieser Berechnung schlussendlich Null rauskommt und dann eine zwischen allen Tasks geteilte Variablen, in dem namens Zero Count aktualisiert. Das heißt, diese Tasks, die eigentlich unabhängig voneinander sind, müssen jetzt sich plötzlich doch synchronisieren, um eben auf diese Variable zugreifen zu können. Das Problem, das wir jetzt hierbei bekommen, ist ein sogenanntes Data Race und das will ich am Beispiel von dem Code hier einfach mal kurz erklären. Also nehmen wir mal an, wir haben zwei Threads, die parallel ausführen und eben jeweils diese fo Methode dann ausführen. Dann schauen wir uns mal ein bisschen genauer an, was bei diesem Zero Count plus plus Statement eigentlich passiert. Was da passiert ist, wie lesen den aktuellen Wert von Zero Count. Das heißt, der wird in irgendein Register gelesen, nennen wir es mal R1. Anschließend wird dann die plus plus Operation oder der Teil der einst dazu addiert ausgeführt. Wir berechnen erstmal das Ergebnis davon und schreiben das dann auch wieder in ein Register. Also sowas. Und schlussendlich wird dann dieses Ergebnis, also der alte Wert plus eins, wieder zurückgeschrieben in diese Zero Count Variable. So, und jetzt passiert das Ganze natürlich nicht nur in einem Thread, sondern auch parallel in dem anderen Thread. Der hat sein eigenes Register, also das heißt jetzt zwar auch R1 hier, aber das ist ein anderes. Und führt dann da aber genau dieselben Instruktionen aus, die wir in Thread 1 schon sehen. Und die Frage ist jetzt, was ist das Problem? Das Problem ist, dass Zugriffe auf diese Zero Count Variable jetzt parallel stattfinden und nicht miteinander synchronisiert werden. Also insbesondere haben wir hier einen Zugriff, wo wir Zero Count lesen, hier einen anderen, wo wir das auch lesen. Die zwei Lesezugriffe an sich wären erstmal kein Problem, weil es ist egal, in welche Reihenfolge die stattfinden, die werden immer dasselbe lesen. Problematisch wird es aber, wenn wir eben auch Schreibzugriffe haben, zum Beispiel den hier unten oder den hier drüben, bei dem ich vergessen habe, hinzuschreiben, was da eigentlich geschrieben wird. Nämlich das Ergebnis von R1, also diesen zweiten Zugriff hier, bei dem in Zero Count geschrieben wird. Und wenn jetzt zum Beispiel nicht klar ist, wer von diesen beiden Zugriffen zuerst stattfindet, dann ist auch nicht klar, was der Lesezugriff auf der linken Seite eigentlich liest. Denn er könnte entweder den alten Wert von vorher lesen oder den neuen Wert nachdem Thread 2 den in Zero Count schon reingeschrieben hat. Und hier nachdem, was dann zuerst passiert, kommt was anderes raus. Sprich, es gibt so eine Art Rennen zwischen diesen beiden Zugriffen und daher der Begriff Data Race. Und in dem Beispiel ist es eben nicht nur zwischen diesen beiden, sondern auch zwischen diesen beiden hier. Also wie der Lese und Schreibzugriff und auch noch zwischen diesen beiden hier unten. Denn wir werden, je nachdem, wer zuerst schreibt, dessen Wert wird dann von dem, der als zweites schreibt, überschrieben. Also gibt es da sozusagen auch wieder ein Rennen darum, wer hier als erstes ankommt. Also jeder dieser drei gestrichelten Linien ist ein Data Race. Nachdem wir jetzt das Beispiel gesehen haben, gebe ich nochmal die etwas allgemeinere Definition für sein Data Race. Also im Prinzip gibt es drei Bedingungen dafür, dass die da sein müssen, damit wir ein Data Race haben. Nämlich zum einen brauchen wir zwei Zugriffe auf dieselbe Memory Location, also auf dasselbe Stück Speicher. Dann muss mindestens einer dieser beiden Zugriffe schreibend sein. Also wenn wir einfach nur zwei Lese-Zugriffe haben, wie gerade in dem Beispiel gesehen, dann kann da erstmal nichts schiefgehen. Und als dritte Bedingung haben wir das, die Reihenfolge dieser Zugriffe nicht deterministisch sein muss. Das heißt, wir wissen nicht, welcher dieser Zugriffe zuerst stattfinden wird. Zum Beispiel, weil eben keine Synchronisierung zwischen den parallelen Threads stattfindet. Und wenn all diese drei Bedingungen gelten, dann haben wir ein Data Race. So, und damit werden wir auch schon am Ende dieser kleinen Einführung. Also ich hoffe, Sie haben jetzt zumindest so ein bisschen eine Idee davon bekommen, welche Probleme denn eigentlich bei Concurrency auftreten. Und in den verbleibenden Videos schauen wir uns dann an, wie das Ganze auf Programmiersprachen eben nicht gelöst wird, beziehungsweise wie Programmieren die Möglichkeit gegeben wird, eben genau diese Probleme zu verhindern. Vielen Dank für's Zuhören und bis zum nächsten Mal.