 Ja, herzlich willkommen zurück zur Veranstaltung Programmierparadigmin. Wir sind hier im fünften und letzten Teil des Modules Control Abstraction und in diesem Teil soll es um Events gehen, also eine anderen Form von Abstraktion mit der bestimmte Theile des Verhaltens eines Programms beschrieben werden können, die es interessanterweise auf verschiedenen Leveln gibt als sowohl im Betriebssystem als auch in Programmiersprachen. Vielleicht fangen wir erst mal an mit der Frage, was ist denn überhaupt so ein Event? Also ein Event ist einfach irgendetwas, das passiert, worauf das Programm reagieren sollte und wir wissen vorher nicht so richtig, wann genau das passiert, sondern es passiert zu einer mehr oder weniger unvorherrsagbaren Zeit. Also ein Beispiel sind alle möglichen Sachen, die vom Benutzer kommen, zum Beispiel Events, die im grafischen Userinterface ankommen, so Dinge wie Mausklicks oder jemand tippt irgendwo was ein. Anderes Beispiel ist, wenn ich input-output Operation habe, zum Beispiel von einer Dateilese oder Dinge aus dem Netzwerkempfang, dann passiert das oft Asynchron, das heißt ich weiß nicht genau, wann das aus dem Netzwerk kommt, ich weiß auch nicht genau, wie schnell ich jetzt tatsächlich von meiner Festplatte lesen kann und all das wird im Programm dann in Events abstrahiert, auf die das Programm reagieren muss, indem es zum Beispiel etwas tut, wenn auf irgendetwas geklickt wurde. Und dieses etwas, was dann getan wird, das wird definiert in einem Event-Händler, was ja eine Form von Control Abstraction ist, nämlich in Form einer Routine, die genau dann aufgerufen wird, wenn eine bestimmte Art von Event passiert. Und hier gibt es zwei Arten von Händern, die wir uns hier ein bisschen anschauen wollen, nämlich zum einen sequenzielle Händler, die sozusagen die aktuelle Ausführung unterbrechen, um dieses Event zu behandeln oder Thread-based Handlers, die in einem parallel ablaufenden Thread geschehen und sozusagen gleichzeitig mit der eigentlichen Ausführung des Programms dann ausgeführt werden. Schauen wir uns zunächst das mal die sequenziellen Händler ein bisschen genauer an. Also hier ist die Idee, dass ein Event behandelt wird und zwar in dem Main Thread der aktuellen Ausführung, sodass diese Ausführung eben unterbrochen wird, das Event dann behandelt wird und dann geht es an der Stelle weiter, wo wir gerade aufgehört haben. Ein Beispiel, wo das verwendet wird, sind die Betriebssystem Interrupt Handlers. Also jedes Betriebssystem hat gewisse Interrupts, also gewisse Dinge, die manchmal passieren können und worauf ein Programm reagieren muss und oder reagieren kann zumindest. Und das wird dann in diesen sogenannten Interrupt Handlers gemacht, die eben dann sequenziell also in dem Hauptthread der Ausführung sofort, wenn das Eventauftritt ausgeführt werden. Das Ganze läuft so, dass ein Programm ein Händler registrieren kann für eine bestimmte Bedingung, für eine bestimmte Art von Interrupt. Und wenn dieser Interrupt dann auftritt und zum Beispiel auf dem Hardware-Level direkt getriggert wird, dann wird das Betriebssystem die aktuelle Kontrolle, also sozusagen die Ausführungskontrolle vom Programm zu dem entsprechenden Interrupt-Händler übergeben und anschließend dann den Zustand, in dem das Programm vorher war, wiederherstellen, so dass es aus Sicht des Programmes so aussieht, als ob es genau da dann weitergeht. Als konkretes Beispiel schauen wir uns einfach mal das Signaling an, wie es auf Unix-basiertem Betriebssystem funktioniert, also zum Beispiel dem Linux, auf dem ich das Ganze hier gerade aufzeichne. Das Betriebssystem, also dem Fall Linux oder Unix definiert eine Liste von Signalen, die jeweils bestimmte Ereignisse beschreiben. Also zum Beispiel ist eines dieser Ereignisse, dass der aktuelle Prozess unterbrochen werden soll, indem ich zum Beispiel versuche diesen Prozess zu killen und was dann mithilfe des SickKill-Signals beschrieben wird. Eine andere Variante ist, dass verschiedene Prozesse miteinander kommunizieren und da gibt es zum Beispiel einen Signal namens SickUserOne, was dafür verwendet werden kann. Ich habe hier mal so eine Liste von diesen Signalen, die es unter Unix üblicherweise gibt. Also jedes dieses Signale hat so ein Name, der mit Sick anfängt, dann gibt es auch noch ein Zahlenwert dazu und dahinter wird dann beschrieben, was sich genau dahinter versteckt. Ich lasse das einfach dann mal in den Folien und man kann sich das einfach hinterher nochmal genauer anschauen. Was jetzt passiert ist, dass jedes Programm ein Händler registrieren kann, mithilfe dessen mit dann das Verhalten, was eigentlich passieren würde, wenn so ein Signal ankommt, überschrieben werden kann. Also standardmäßig wird zum Beispiel, wenn ich SickKill aufrufe, der Prozess einfach abgebrochen, aber ich kann auch ein Händler registrieren und überschreiben, dass da vielleicht was anderes passiert, zum Beispiel, dass der Prozess eben doch nicht abgebrochen wird. Diese Signale werden asynchron überliefert. Das heißt, der Programm Zustand wird einfach in dem Moment dann gespeichert, wenn so ein Signal auftritt und das Programm pausiert. Der registrierte Händler wird dann ausgeführt, wo auch immer das Programm gerade war und anschließend geht es dann hinterher an der Stelle weiter, wo das Programm war, als dieses Signal angekommen ist. Also schauen wir uns mal ein bisschen genauer an, was denn passiert, wenn in dem Betriebssystem so ein Event, so ein Signal auftritt und wie das dann tatsächlich zum Eventhändler einer bestimmten Applikation übergeben wird. Also in diesem Ganzen spielen eigentlich zwei komponenten eine Rolle, nämlich zum einen der Betriebssystem Kernel, also zum Beispiel der Linux Kernel und zum anderen die Anwendung, also die User Application, die auf diesem Betriebssystem läuft. Wir haben in der Anwendung irgendwo unsere ganz normale Programmausführung, die ich jetzt hier einfach mal so als eine Sequenz von Instruktion darstellt, also dass wir die ganz normale Ausführung des Benutzerprogramms und was jetzt irgendwann passieren kann, ist, dass so ein Event auftritt und wir deshalb das Nutzerprogramm unterbrechen müssen und das wird mithilfe eines sogenannten Hardware Interrupts gemacht, der auf der CPU der aktuellen Ausführung gesagt stopp, du musst unterbrechen, denn hier passiert irgendwas anderes und was dann passiert ist, dass die Kontrolle übergeben wird, also dass die Ausführung des Programms wird an genau der Stelle, wo wir gerade sind, unterbrochen und stattdessen wird die Kontrolle an das Betriebssystem übergeben, indem wir den Interrupt-Händler haben, der dann diesen Interrupt tatsächlich behandelt. Und was der als erstes macht, ist, dass der den Zustand des Benutzerprozesses speichert, sodass wir später dann wieder zurück können dahin, wo wir vorher gewesen sind. Anschließend kommen wir wieder zurück aus dem Betriebssystemkönnen, also wir gehen von diesen Interrupt wieder zurück und wir gehen jetzt aber nicht direkt wieder in das eigentliche Programm rein, sondern haben jetzt einen Event-Händler, der eben diesen Interrupt oder dieses Signal behandeln soll. Dieser Event-Händler ist im Prinzip ein Stück Code, der irgendwo in dem Programm mal definiert wurde. Und dieses Stück Code wird jetzt aufgerufen, genauso wie eine Funktion auch aufgerufen wird. Und um das tun zu können, müssen wir natürlich diese ganze Corning Sequence, die wir früher in dem Modul hier schon gesehen haben, ausführen. Also all das, was geschieht, um eine Funktion aufzurufen, muss eben jetzt auch hier geschehen, um diesen Event-Händler aufzurufen. Das heißt, wir brauchen ein Stück Code, der ausgeführt wird, bevor wir in den eigentlichen Event-Händler reinspringen können und jetzt zum Beispiel unsere Dinge macht, wie die aktuellen Register zu speichern oder den Frame-Pointer und den Base-Pointer anzupassen, sodass wir dann diese Event-Händler Funktion aufrufen können. Und dieses Stück Code nennt sich Signal Trampolin und der Name Trampolin kommt im Prinzip daher, dass das im Prinzip so eine Art Trampolin ist, wo wir bloß kurz reinspringen, um anschließend wieder rauszuspringen, nämlich in diesen Event-Händler. Also wenn man sich den Kontrollfluss anschaut, springen wir im Prinzip direkt hier von dem Interrupt-Händler in dieses Signal Trampolin rein und von hier dann direkt wieder raus zum Event-Händler, der aufgerufen wird, genauso wie wir eine andere Funktion auch aufrufen würden. Und was der Event-Händler dann macht, ist dieses Signal oder Event zu behandeln und schlussendlich irgendwann wieder zurückzukehren zu unserem Signal Trampolin. Und was das dann schlussendlich macht, ist den normalen Zustand, wie er vor dem Interrupt existiert hat, wieder herzustellen, sodass wir dann schlussendlich von hier wieder in unser Hauptprogramm zurück können und die Ausführung da hoffentlich ganz normal fortgesetzt werden kann. So, um das Ganze nochmal ein bisschen praktisch zu zeigen, möchte ich mal zwei kleine Programme zeigen, die hier auf meinem Linux laufen und werde denen dann ein bisschen das Signal schicken und wir werden sehen, wie die darauf reagieren. Das erste ist ein sehr, sehr sinnvolles Programm, denn das hat genau ein Statement und das ist eine Wildschleife, die immer weiter ausgeführt wird, weil die Bedingung einfach dauerhaft eins ist. Das heißt, dieses Programm läuft einfach, sofern ich es dann nicht unterbreche, unendlich lang und wird auch nie aufhören. Ich kompiliere das mal und füße dann aus und wie man sieht, läuft das Programm und kehrt auch erst mal nicht zurück. So, was ich jetzt machen kann, ist, ich kann diesem Signal jetzt, diesem Programm einen Signal schicken und das Programm glauben lassen, dass irgendetwas passiert ist, was eben in diesem Signal drin steht. Eine Sache, die ich dafür brauche, ist die Prozess-ID von dem Programm, also suche ich mal kurz nach dem Programm, was hier läuft. Die Prozess-ID ist also das und jetzt kann ich dem Programm mithilfe des Kill-Kommandos, zum Beispiel das SICK-Sacken-Signal schicken und was das tut, ist, dass ich, wenn ich jetzt wieder zurück zu meinem ersten Terminal gehe, dem Programm sagt, hey du bist gerade abgebrochen, weil du eben ein Segmentation-Fault hattest und das ist natürlich nicht wirklich passiert in dem Programm, weil das hat ja eigentlich gar keine Statements, die zu einem Segmentation-Fault führen könnten, aber ich habe dem Programm dieses Signal geschickt und der Prozess glaubt das dann sozusagen und ist damit unterbrochen, weil er denkt, es hat gerade ein Segmentation-Fault gegeben. Als zweites Beispiel möchte ich mal ein anderes Programm zeigen, in dem wir nicht einfach nur auf Signale blind reagieren, sondern explizit ein Signal-Händler schreiben und zwar ist es auch wieder so ein sehr sinnvolles Programm, was einfach nur so eine Endlosschleife hat, aber neben dieser Schleife auch noch ein Signal-Händler registriert und zwar für dieses SIGINT-Signal und wenn das kommt, dann möchte ich diese Hello-Funktion aufrufen, die Hello-World ausgibt. Dieses SIGINT-Signal, also in steht für Interrupt, geschieht genau dann, wenn ich Ctrl-C drücke, also wenn ich das Programm laufen habe und dann Ctrl-C drücke, also normalerweise das Programm abstürzen würde, dann wird das Programm jetzt in dem Fall nicht abstürzen, sondern ich rufe dann diesen Signal-Händler hier oben auf und gebe dann hoffentlich Hello-World aus. Schauen wir mal, ob das der Fall ist, also ich kompiliere das zweite Programm und führe es dann auch aus und jetzt läuft es erst mal wieder in seiner Endlosschleife und wenn ich jetzt Ctrl-C drücke, dann wird normalerweise das Programm beendet, aber in dem Fall eben nicht, weil ich eben diesen Signal-Händler registriert habe, der stattdessen einfach noch Hello-World ausgibt und mir sozusagen kundtut, dass ich das Programm jetzt nicht so einfach unterbrechen kann. Zum Glück gibt es noch ein anderes Signal, nämlich wenn ich Ctrl-Bag drücke, dann wird das Quit-Signal an den Prozess gesendet und dafür gibt es hier zum Glück kein Händler, den ich registriert habe und damit wird das Programm dann schlussendlich natürlich trotzdem abgebrochen. Also was man hier sieht, ist einfach, dass jedes Programm, was ich schreibe, dieses Signal-Händler registrieren kann und dann entsprechend auf diese vom Betriebssystem versandten Signale reagieren kann. So, jetzt haben wir viele über diese sequenziellen Händler geredet. Ich möchte auch noch ganz kurz was zu den Thread-Based-Händlers sagen. Also hier ist die Idee eben nicht, dass das Hauptprogramm unterbrochen wird, wenn ein Signal oder ein Event geschieht, sondern ich habe einen bestimmten Thread, der irgendwo im Hintergrund läuft und der nur dafür verantwortlich ist, bestimmte Arten von Events zu behandeln. Das macht man im Prinzip häufig, wenn man genau einen solchen Thread hat. Es könnte auch mehr als eingeben, aber in der Regel gibt es genau einen solchen Thread und die Idee davon ist, dass dieser Thread alles macht, was mit einer bestimmten Art von Events zusammenhängt, zum Beispiel alles, was mit dem grafischen User-Interface, der GUI zu tun hat. Und weil es eben genau diesen einen Thread ist, muss der sich auch nicht mit anderen Dingen synchronisieren, sondern der behandelt alles, was mit der GUI zu tun hat und kann das dann auch machen, ohne dass da jemand anders dazwischenfunkt. Dieses Pattern, dass man eben diesen einen Thread hat, der für bestimmte Arten von Events zuständig ist, findet man zum Beispiel bei Android-Apps, wo es eben genau einen UI Thread gibt, der dafür verantwortlich ist, die UI der App zu aktualisieren und alles, was UI-spezifisch ist, muss eben durch diesen Thread durchgehen. Im Falle von Android ist dieser UI Thread auch gleichzeitig der Main-Thread, der Applikation. Das heißt, man darf in diesem Thread im Prinzip oder man sollte in diesem Thread auch nur sehr kurze Operationen ausführen, denn wenn ich da jetzt zum Beispiel eine längere Operation habe oder vielleicht sogar so eine Endlust-Schleife, wie die, die wir gerade gesehen haben, dann wird die App plötzlich nicht mehr reagieren. Das heißt, ich klick dann vielleicht irgendwo drauf oder tippe irgendwo drauf und es passiert aber nichts, ganz einfach, weil dieser eine Thread, der auf die UI Events reagiert, in dem Moment dann beschäftigt ist und vielleicht gerade dieser Schleife ausführt. So, zum Abschluss dieses Modules zum Thema Control Abstractions habe ich noch ein kleines Quiz und zwar wieder in der Form, dass wir hier vier Sätze haben, von denen manche stimmen und manche nicht stimmen. Und ich würde Sie bitten, einfach an der Stelle das Video dann anzuhalten, bisschen darüber nachzudenken, welche dieser Sätze stimmen, im Ilias abzustimmen und anschließend dann erst weiterzuschauen. So, schauen wir uns mal die Lösung an. Also, die ersten zwei Sätze stimmen nicht, denn Koroutinen sind eben nicht für Preemptive Multitasking, sondern für Non-Preemptive Multitasking, also in der Form von Multitasking, wo die einzelnen Tasks nicht unterbrochen werden, sondern explizit selber die Kontrolle zu anderen Tasks oder Koroutinen transferieren. Eine Calling Sequence ist eben nicht die Liste der Subroutinen, die während der Ausführung ausgeführt werden, sondern sind die Sequenzen von Code, die vor und nach dem Aufruf einer Funktion ausgeführt werden müssen. Und die letzten beiden Sätze stimmen wieder, finally closes werden tatsächlich immer ausgeführt, egal ob eine exception geworfen wird oder nicht. Und Signals, eine Form von Events, wie wir sie im Betriebssystem oft haben, unterbrechen die normale Ausführung und dann wird entsprechend der Signalhändler ausgeführt. Ja, und damit sind wir ja auch schon wieder am Ende dieses Moduls. Ich hoffe, Sie wissen jetzt ein bisschen besser darüber, Bescheid, welche Form von Control Abstraction es denn in Programmiersprachen gibt, also wie ich quasi das Verhalten oder ein bestimmtes Stück von Verhalten in eine Abstraktion kapseln kann, zum Beispiel Subroutinen oder Koroutinen oder so Dinge wie Exception-Händler oder Event-Händler. Vielen Dank fürs Zuhören und bis zum nächsten Mal.