 Ja, herzlich willkommen zurück zu Programmierparadigmen. Wir sind hier in Teil 4 von 5 des Moduls Control Abstraction und was wir in diesem 4. Teil machen wollen, ist uns Co-Routine anzuschauen. Was es ist, werden wir gleich sehen, aber ganz kurz und knackig gesagt, ist das im Prinzip ein Weg, wie man fast parallele Ausführungsstränge beschreiben kann, die aber eben nicht unterbrochen werden können, sondern explizit miteinander interagieren. Co-Routinen sind also eine Form von Control Abstraction, also so ein bisschen so etwas Ähnliches wie Routinen. Allerdings erlauben die, während der Ausführung die Ausführung anzuhalten und den Kontrollfluss an eine andere Co-Routine zu übergeben und dann später an der Stelle, wo sie sich selbst unterbrochen hat, weiterzumachen. Was man damit also ausdrücken kann, ist, was man non-preemptive multitasking nennt. Das heißt, ich habe also mehrere parallele ausführende Ausführungsstränge, die aber nicht unterbrochen werden können, sondern kooperativ miteinander interagieren und gelegentlich an den richtigen Stellen, die die Ausführung an einen anderen Task, der auch parallel ausgeführt wird, übergeben. Schauen wir uns am besten erst mal ein Beispiel an, an dem wir sehen können, was diese Co-Routinen genau sind und was man damit so machen kann. Ich schreibe das Ganze in so eine Form von Pseudocode auf und werde dabei erläutern, was dieser Code eigentlich genau bedeutet. Also die Idee ist, dass wir so eine Art Bildschirm-Schone implementieren wollen, der zwei Sachen parallel machen soll, nämlich zum einen den Bildschirm aktualisieren, sodass da irgendwas Schönes zu sehen ist und zum anderen im Hintergrund das Data-System des Computers ein bisschen aufzuräumen, zum Beispiel zu D fragmentieren. Das Ganze funktioniert so, dass wir zwei Co-Routinen haben, die diese zwei Aktivitäten darstellen, nämlich zum einen mal die Co-Routine US, was für Update Screen steht und zum anderen die Co-Routine CFS, was für Check File System steht. Beide sind hier deklariert als Co-Routine und was wir dann in unserer Main-Methode machen werden, ist, dass wir diese beiden Co-Routinen aufrufen. Also wir rufen zunächst diese eine hier auf bzw. wir erstellen sie, indem wir sagen, wir hätten gerne eine Co-Routine vom Typ Update Screen und wo dieser Typ ist, das werden wir gleich sehen und schreiben diese Co-Routine dann in die Variable US und gleichzeitig oder danach erstellen wir noch diese andere Co-Routine namens Check File System und auch die werden wir gleich genauer anschauen und erstellen die hier. Und dann müssen wir eine von den beiden anstoßen und sozusagen den Kontrollfluss in eine dieser beiden Co-Routine reingeben und das machen wir mit Hilfe dieser Transfer Funktion und hier sagen wir zum Beispiel, weil es ja ein Bildschirmschoner ist, wird zunächst erstmal der Bildschirm beschrieben, indem wir diese US, also Update Screen, Co-Routine aufrufen. Jetzt müssen wir noch anschauen, was genau diese Co-Routine eigentlich machen und das machen wir, indem ich die hier oben dann definiere. Ich fange mal an mit der zweiten, also die, die Check File System heißt und im Prinzip während des Bildschirmschoner läuft, das Datei System aufrufen soll, während aber gleichzeitig der Bildschirmschoner das macht, was er machen soll, nämlich den Bildschirm aktualisieren. Zunächst erstmal verwende ich hier dieses Detach Kommando und was das macht, das im Prinzip einfach nur eine neue Co-Routine zu erstellen und eine Referenz darauf dann an den aufrufenden Code zurückzugeben. Der aufrufende Code in dem Fall ist das hier unten, das heißt also das, was ich hier in diesem CFS dann habe, ist eben eine Referenz auf diese neu erstellte Co-Routine. So, jetzt schauen wir an, was die Co-Routine dann eigentlich macht, die soll ja unser Datei System ein bisschen aufräumen. Also wir haben da vielleicht so eine Schleife drin, die über alle Dateien iteriert, dann irgendwas mit diesen Dateien macht, was genau ist hier erstmal irrelevant und anschließend dann aber, weil ja parallel dazu auch noch die andere Co-Routine, die ich gleich aufschreibe, laufen soll, nämlich die, die den Bildschirm aktualisiert, übergibt sie die Kontrolle dann regelmäßig an diese andere Co-Routine, nämlich an die, die in der Variable US gespeichert ist. Und davor und danach macht sie irgendwas mit den Dateien, aber entscheidend ist, dass sie explizit sagt an bestimmten Stellen, jetzt geht die Kontrolle bitte an die andere Co-Routine und da soll es dann weitergehen und solange ist die erste Co-Routine unterbrochen und wenn wir dann wieder zurückkehren, geht es genau an der Stelle hinter dem Transfer Statement weiter. Die zweite Co-Routine sieht im Prinzip so ähnlich aus, ich definiere die jetzt mal hier, also die heißt Update Screen und macht am Anfang im Prinzip das Gleiche wie die erste, nämlich dieses Detach aufrufen, was eben dann schlussendlich die Referenz auf diese neu erstellte Co-Routine hier unten zurückgibt und in dieser zweiten Co-Routine haben wir dann auch wieder eine Schleife, die irgendwas mit dem Bildschirm macht und da vielleicht was Schönes draufmalt und periodisch aber auch wieder die Kontrolle an die erste Co-Routine, nämlich die, die in CFS gespeichert ist, zurückgibt und somit die Kontrolle zwischen diesen beiden Co-Routinen hin und her gibt und beide Tasks aber parallel etwas bewirken und die Fortschritt in ihrer eigenen Aufgabe haben aber explizit den Kontrollfluss immer wieder hin und her geben. Jetzt könnte man sagen, diese Co-Routine sieht ja eigentlich so aus wie Threads, die laufen da so mehr oder weniger parallel und arbeiten jeder an ihrer Aufgabe, was ist denn da eigentlich der Unterschied? Also es gibt zwei Unterschiede, die hier wichtig sind, das eine ist, dass bei den Co-Routinen der Kontrollfluss explizit übergeben wird. Wir haben diese Transfer-Statements und nur wenn die Co-Routine sagt, jetzt bitte mich unterbrechen und in der anderen Co-Routine weitermachen, dann geschieht das tatsächlich, wohingegen bei Threads das ganze implizit passiert und zwar auf eine Art und Weise, die prämtiv ist, also wo ein Thread einfach unterbrochen wird, ohne dass er das explizit gewollt hat oder angegeben hat. Der zweite Unterschied ist, dass bei einer Co-Routine immer nur eine zu einer bestimmten Zeit läuft. Also in dem Beispiel hatten wir zwei Co-Routinen und die laufen so pseudo-parallel, aber de facto läuft immer nur eine, weil ich eben den Kontrollfluss hin und her gebe zwischen den beiden. Also es gibt keine echte Nebenläufigkeit, sondern es läuft immer nur eine Co-Routine. Im Gegensatz dazu laufen Threads ja tatsächlich nebeneinander und zur gleichen Zeit, zumindest wenn ich mir eine CPU-Course habe und können dementsprechend dann auch zu so Problemen wie Data Races und so weiter führen, die wir später in der Vorlesung auch noch genauer anschauen werden. Ein anderes Konstrukt, was wir in einer früheren Vorlösung ja schon gesehen haben, was auch so ein bisschen ähnlich zu den Co-Routinen ist, sind die Continuations. Hier sehen wir mal, wie sich diese beiden voneinander unterscheiden. Also bei den Co-Routinen ist es so, dass sich der Zustand dieser Co-Routine ja jedes Mal ändert, wenn ich sie aufrufe. Also die Co-Routine fängt nicht immer wieder am Anfang an, sondern führt, die Ausführung geht genau da weiter, wo ich zuletzt in der Co-Routine war. Das heißt, sie verändert sich kontinuierlich. Und im Gegensatz dazu ändert sich so eine Continuation eigentlich nicht, also die wird einmal erstellt und ist dann also eine Funktion mit ihrem umgebenden Zustand und der ist dann so, wie er ist und verändert sich auch nicht. Ein anderer Unterschied ist, was passiert, wenn ich in so eine Co-Routine oder eine Continuation reingehe. Also bei einer Co-Routine wird der alte Program-Counter, also sozusagen, dass die Adresse im Code, wo ich gerade bin, wird gespeichert. Denn wenn ich zu einer anderen Co-Routine transferiere, dann muss ich ja schlussendlich wieder zu dem selben Stück Code zurückkommen, wo ich gerade in der ersten Co-Routine war und deswegen wird der alte Program-Counter gespeichert. Im Gegensatz dazu ist der aktuelle Program-Counter einfach verloren, wenn ich eine Continuation aufrufe, weil ich bei einer Continuation ja einfach nur sage, der Kontrollfuss geht jetzt genau da in der Continuation weiter und ich kehre auch nie dahin zurück, wo ich hergekommen bin. Deswegen muss ich den alten Program-Counter da auch nicht speichern. Und schlussendlich ist die Frage, was passiert, wenn ich wieder zurückkomme in eine Co-Routine bzw. dann nochmal reingehe. Also bei Co-Routine ist es so, dass die Ausführung genau da weitergeht, wo man aufgehört hat, weil man wieder zurücktransferiert. Und im Gegensatz dazu passiert bei Continuations folgendes, wenn ich die mehrmals aufrufe, dann fängt es immer wieder am Anfang an, denn ich bin ja dann immer wieder am Anfang dieser Continuation. Also die sind ein bisschen verwandt und haben auch einige Gemeinsamkeiten, aber es gibt doch eine ganze Menge Unterschiede hier. Die Gemeinsamkeit von Co-Routine und Continuations ist, dass beide durch eine Closure repräsentiert werden und nur nochmal zur Erinnerung. Eine Closure ist sozusagen ein Stück Coat mit einer Adresse, wo in dem Coat wie jetzt als nächstes ausführen wollen und dem sogenannten Referencing Environment, was also Namen an bestimmte Speicheradressen bindet. In dem früheren Teil dieses Moduls haben wir ja gesehen, wie der Function Stack aussieht und wenn man Co-Routine hat, entwickelt sich das auf eine interessante Art und Weise. Natürlich kann jede Co-Routine auch weitere Subroutine aufrufen oder auch andere Co-Routine erstellen und gleichzeitig hat natürlich dann jede Co-Routine ihren eigenen Function Stack. Und das führt im Prinzip dazu, dass wir mehrere Function Stacks parallel haben, die sozusagen den entsprechenden, ja den jeweiligen Co-Routine dann entsprechen. Und wenn eine Co-Routine jetzt eine andere Subroutine aufruft, wird die einfach auf den Stack dieser Co-Routine dann drauf gebaut. Und weil wir diese verschiedenen Stacks dann parallel haben, ergibt sich ein sogenannte Cactus Stack und warum der so heißt, wie er nun mal heißt, werden wir gleich sehen. So, schauen wir uns mal ein Beispiel dafür an, wie mehrere Co-Routinen und Subroutinen miteinander interagieren können und was das dann für Auswirkungen auf den Stack hat und was wir da sehen werden, ist eben diese sogenannte Cactus Stack. Für das Beispiel gehen wir davon aus, dass wir eine Programmiersprache haben, in der wir Routinen ineinander verschachteln können und in der wir nicht explizit angeben müssen, ob eine Routine jetzt eine Subroutine oder eine Co-Routine ist. Deswegen schreibe ich hier auch erstmal bewusst einfach nur Routine hin und werde jetzt mal aufschreiben, wie die im Code ineinander verschachtelt sind. Also wir haben da so auf dem obersten Level drei Routinen, nämlich A, M und Q und innerhalb von denen gibt es jetzt wieder weitere Routinen. Zum Beispiel ist hier in A drin noch P reingeschachtelt, in M haben wir B und S reingeschachtelt und in Q haben wir zwei Routinen noch drin, nämlich C und R. Und außerdem gibt es hier noch ein drittes Level, nämlich in B drin gibt es noch eine Routine, nämlich die hier namens D. So, das ist jetzt erstmal nur der Source Code, so wie ihr im Programm steht und jetzt müssen wir uns nochmal eine konkrete Ausführung dieses Codes anschauen, wo wir davon ausgehen, dass als allererstes diese Routine namens M aufgerufen wird und was M macht, ist drei Sachen. Zum einen erstellt M eine Subroutine, die dann A ist. Dann ruft sie selbst eine, ah sorry, also A ist eine Koroutine, keine Subroutine, dann ruft sie eine Subroutine auf namens Q. Das Ganze passiert im, also die Q-Subroutine wird im selben Stack sein wie M, weil es einfach eine Subroutine ist, wohingegen A einen neuen Callstack bekommt, weil es eben eine Koroutine ist. Und für das dritte, was M macht, ist noch eine weitere Koroutine zu erstellen, nämlich B. Und weil das eine weitere Koroutine ist und jede Koroutine dann ihren eigenen Function-Stack hat, ergibt sich da hier so ein weiteres Stack. Diese ganzen Routinen können natürlich dann wieder weitere Sachen aufrufen, zum Beispiel gehen wir mal davon aus, dass A diese Routine P aufruft und das Q eine weitere Routine namens R aufruft und gleichzeitig noch eine weitere Koroutine erstellt, nämlich C. B macht sowas ähnliches, das ruft nämlich auch eine weitere Subroutine auf namens S und erstellt auch noch eine Koroutine, nämlich D. Und wie man an den Bildern sieht, ergeben sich daraus mehrere Stacks, die sozusagen parallel existieren. Die eigentliche Ausführung ist, weil es eben Koroutinen sind, immer nur in einer dieser Stacks drin, aber die Stacks haben jeweils ihren eigenen Zustand und haben im Prinzip genau das, was wir im Function-Stack ja auch schon gesehen haben, also all die lokalen Variablen und so weiter. Die sind jeweils in diesen Stack-Segmenten dann gespeichert. Eine interessante Frage ist jetzt noch, wie die Namen der lokalen Variablen in diesen verschiedenen Routinen schlussendlich aufgelöst werden. Und was wir da ja in einer der früheren Vorlesungen schon gesehen haben, sind die sogenannten Static-Links, die jeden, jeder Funktion sagen, wo denn ihre, ja, so ein taktisch umgebende Funktion ist. Und was wir hier sehen werden, ist, dass der Static-Link nicht unbedingt immer einfach nur zum 1 drüberstehenden Funktion oder in unserem Fall eigentlich drunterstehenden Funktion im Call-Stack geht, sondern eben zu der Funktion, die lexikalisch wirklich um die Funktion herum ist. Also im Falle von P zum Beispiel wird der Static-Link auf A zeigen, im Falle von C wird der Static-Link auf Q zeigen, genauso bei R, weil sowohl C als auch R in Q genestet sind. Im Falle von D zeigt der Static-Link auf B, der von S zeigt allerdings auf M, also nicht auf das, was einfach drunter steht und genauso bei B, der von B zeigt auch auf M. Das heißt, mithilfe dieser Static-Links können die einzelnen Funktionen dann ihre lokalen Namen auflösen bzw. dann auch die Namen finden, der lexikalisch um sie herum befindlichen Funktionen. So, jetzt haben Sie dieses Konzept von Coroutine so in der Theorie hoffentlich verstanden. Jetzt ist die Frage, in welchen Sprachen gibt es das denn eigentlich und wo kann man das denn tatsächlich verwenden? Also ein paar populäre Sprachen, die Coroutine in irgendein Form anbieten, sind hier mal aufgelistet. Also es gibt Sprachen, in denen das Ganze nativ unterstützt wird, also wo Coroutine tatsächlich Teil der Sprache sind. Das ist zum Beispiel in Ruby oder Go, der Fall in Go heißen die dann nicht Coroutine, sondern Go-Routine interessanterweise. Also das sind Sprachen, wo man das wirklich einfach direkt benutzen kann, weil das Teil der Sprache ist. In vielen anderen Sprachen gibt es Coroutine in Form von Bibliotheken, die man verwenden kann, die das sozusagen emulieren, obwohl es nicht Teil der Sprache an sich ist. Das ist zum Beispiel für Java, C-Sharp, JavaScript oder Kotlin der Fall, wo es gute Bibliotheken gibt, mit denen man Coroutine verwenden und emulieren kann. Und da gibt es in manchen Sprachen noch so spezialisierte Varianten von Coroutine, also die Generators, die wir ja im Zusammenhang mit den Iteratoren auch schon gesehen haben, sind eine Form von Coroutine und die gibt es dann zum Beispiel in Python, also das ist keine Coroutine so in ihrer vollen Blüte, sondern eine spezielle Variante, die eben einfach nur eine Liste von Werten generiert, aber im Prinzip auch dasselbe Programmiersprachenkonzept dahinter hat, nämlich die Coroutine. Ja und damit sind wir auch schon wieder am Ende dieses vierten Teils im Modul Control Abstraction. Ich hoffe, Sie wissen jetzt ein bisschen mehr darüber, was eigentlich Coroutine sind und wer das nochmal genauer ausprobieren will, der sollte sich eine dieser Implementierungen, die ich gerade genannt habe, vielleicht mal anschauen und einfach ein bisschen damit rumspülen, um ein bisschen Gefühl dafür zu kriegen, was man damit alles machen kann. Vielen Dank fürs Zuhören und bis zum nächsten Mal.