Technische Universität München Institut für Informatik Lehrstuhl für Computer Graphik & Visualisierung Praktikum: Grundlagen der Programmierung Prof. R. Westermann, R. Fraedrich, R. Fülöp, H.G. Menz WS 2009 Aufgabenblatt 12 28. Januar 2010 Abgabe: In KW 6/7 (4. Februar – 10. Februar) vor der Übung. Threads und Synchronisierung 12.1 (Ü) Allgemeine Fragen (a) Was sind Threads und wozu dienen Sie? (b) Laufen Threads grundsätzlich parallel (d.h. gleichzeitig) ab? (c) Was macht der folgende Java-Code? public class HelloWorldRunner implements Runnable { public void run () { System . out . println ( " Hello World ! My Name is " + Thread . currentThread (). getName ()); } public static void main ( String [] args ) { Thread t = new Thread ( new HelloWorldRunner () , " Hello World Thread " ); t . start (); } } (d) Was ist der Unterschied, wenn statt t.start() t.run() stehen würde? (e) Was machen die Methoden Thread.sleep(long milliseconds) und Thread.yield()? (f) Bei den beiden genannten Methoden handelt es sich um statische Methoden. Auf welchen Thread wirkt sich der Aufruf dieser Methoden auf, wenn sie in der Methode run() oder in der Methode main(...) ausgeführt werden würden. (g) Diskutieren Sie den folgenden Code-Ausschnitt. Was macht dieses Programm und wie wird der Thread beendet? 1 public class WaitThread implements Runnable { public void run () { while (! Thread . interrupted () ) { try { System . out . println ( " I am still alive . I will sleep for a second ... " ); Thread . sleep (1000); } catch ( I n t e r r u p t e d E xception e ) { // The exception resets the flag -> call interrupt () to set flag again Thread . currentThread (). interrupt (); } } System . out . println ( " I ’ ve been interrupted and will terminate myself . " ); } public static void main ( String [] args ) { Thread t = new Thread ( new WaitThread () ); t . start (); Thread . sleep (3000); t . interrupt (); // signals the thread to terminate } } 12.2 (Ü) Synchronisation (a) Welche Werte können Threads untereinander lesen und schreiben? (b) Worauf muss beim Schreiben gemeinsamer Werte geachtet werden? (c) Was bezeichnet bei Threads eine Race Condition bzw. einen kritischen Wettlauf ? Nennen Sie Beispiele welche Fehler dabei auftreten können? (d) Was bezeichnet hierbei einen kritischen Abschnitt? Wie werden die kritschen Abschnitte in den beiden nachfolgenden Beispielen geschützt? public class TicketBooking implements Runnable { protected static int glo balTicketCount = 10; protected int ticketsToBuy ; public TicketBooking ( int numTicketsToBuy ) { ticketsToBuy = numTicketsToBuy ; } protected static synchronized boolean buyTickets ( int numTickets ) { if ( globa lTick etCou nt >= numTickets ) { glo balTi cketCo unt -= numTickets ; return true ; } return false ; } 2 public void run () { if ( ! buyTickets ( ticketsToBuy ) ) System . out . println ( " Sorry , not enough tickets available ! " ); } public static void main ( String [] args ) { Thread t1 = new Thread ( new TicketBooking (4) ); Thread t2 = new Thread ( new TicketBooking (3) ); t1 . start (); t2 . start (); } } class TicketBooking2 implements Runnable { protected static Integer globalTicketCount = 10; protected int ticketsToBuy ; public TicketBooking2 ( int numTicketsToBuy ) { ticketsToBuy = numTicketsToBuy ; } public void run () { synchronized ( globa lTicketCount ) { if ( globa lTick etCou nt >= ticketsToBuy ) { glo balTic ketCo unt -= ticketsToBuy ; } else System . out . println ( " Sorry , not enough tickets available ! " ); } } public static void main ( String [] args ) { Thread t1 = new Thread ( new TicketBooking2 (4) ); Thread t2 = new Thread ( new TicketBooking2 (3) ); t1 . start (); t2 . start (); } } 12.3 (Ü) Hallo Echo. . . Im Folgenden sollen Sie ein Programm schreiben, dass Ihnen zu einer Eingabe ein “virtuelles Echo” erzeugt. Die eingegebene Zeichenkette soll dabei sekündlich auf der Konsole wiederholt werden, wobei die Zeichenkette bei jeder Wiederholung um zwei Zeichen von vorne verkürzt wird. Beispielsweise soll die Eingabe Hallo Echo, die folgende Ausgaben erzeugen: llo Echo o Echo Echo ho 3 Echo von einer einzelnen Eingabe Um das Echo für eine Zeichenkette zu erzeugen soll ein Thread verwendet werden. Die Implementierung soll dabei in der Klasse Echo erfolgen die vom Interface Runnable abgeleitet wird und deren Konstruktor die zu wiederholende Zeichenkette übergeben bekommt. In der Methode run() soll der Thread jeweils eine Sekunde pausieren, bevor die nächste verkürzte Zeichenkette ausgegeben wird. Sobald die Zeichenkette soweit verkürzt ist, dass nichts mehr ausgegeben wird, soll sich der Thread von selbst beenden. Der Rumpf für das Einlesen einer Zeichenkette ist bereits in der main-Methode vorhanden. Nach dem Einlesen soll für die eingelesene Zeichenkette der Echo-Thread erzeugt und gestartet werden. Testen Sie Ihr Programm nun mit verschiedenen Eingaben. Echo von mehreren Eingaben Nun soll das Programm so erweitert werden, dass auch mehrere Eingaben hintereinander erlaubt sind, für die jeweils ihr Echo erzeugt wird. Wenn die Eingaben schnell genug hintereinander sind, sollte dies zu einer Verschachtelung der Echos führen. Ändern Sie die main-Methode so ab, dass solange Zeichenketten eingelesen und der dazugehörige Thread erzeugt und gestartet wird, bis der Benutzer “stop” eingibt. Testen Sie wiederum ihr Programm mit verschiedenen Eingaben und prüfen Sie, ob Ihr Programm beendet wird, sobald Sie “stop” eingeben. Beenden aller Threads Falls Sie “stop” eingegeben haben, während ein Echo noch wiederholt wurde, werden Sie bemerkt haben, dass zwar die main-Methode dadurch beendet wurde, die übrigen Threads aber solange fortgesetzt wurden, bis das Echo ausgelaufen ist. Ändern Sie Ihr Programm nun so ab, dass bei der Eingabe von “stop” alle Threads inklusive der Echos unmittelbar beendet werden. Merken Sie sich dazu alle erzeugten Echo-Threads in einer Liste und teilen Sie beim Beenden des Programms allen Threads mit Hilfe der Funktion interrupt() mit, dass sie sich selbst beenden sollen. Ändern Sie die Methode run() entsprechend so ab, dass der Thread sich nach dem Interrupt selbst beendet. 12.4 (Ü) Kekse In dieser Aufgabe sollen Sie ein einfaches Producer-Consumer -Szenario realisieren. Zunächst benötigen Sie Objekte, die produziert und konsumiert werden können: Erstellen Sie dazu eine Klasse Cookie mit einer Methode public void eat(). Nun brauchen Sie Producer- und Consumer-Threads: Legen Sie die Klassen Mama und Child an, die das Runnable-Interface implementieren. Beide Klassen erhalten in ihrem Konstruktor eine Referenz auf einen Stapel mit Cookies, welche sie in einem Member speichern. Beide Klassen enthalten in ihrer run()-Methode eine Endlosschleife. Die Mama-Klasse testet dabei immer wieder, ob weniger als z.B. 30 Plätzchen vorhanden sind. Wenn dem so ist, wird ein neues Plätzchen angelegt und auf den Stapel gelegt. Die Kind-Klasse testet immer wieder, ob mindestens ein Plätzchen auf dem Stapel liegt. Wenn ja, wird es vom Stapel genommen und gegessen. Legen Sie eine main-Methode an. Legen Sie dort einen leeren Stapel mit Keksen an. Legen Sie 4 einen Mutterthread und 3 Kinderthreads an, die alle Zugriff auf den Keksstapel haben. Starten Sie alle 4 Threads. Einer oder mehrere Kind-Threads werden recht schnell mit einer EmptyStackException abstürzen. Warum? Reparieren Sie das Programm, so dass endlos Plätzchen produziert und konsumiert werden, ohne dass ein Thread abstürzt. 12.5 (H) Threads beim Raytracing (27 Punkte) In den bisherigen Aufgaben mit dem Raytracer wurde ein einzelner Thread dafür verwendet das Bild zu erzeugen und es in einem Fenster anzuzeigen. Entsprechend wurde das Bild nicht angezeigt bis das Bild komplett erstellt wurde. Da der Raytracer allerdings die Farbwerte der einzelnen Pixel nach und nach bestimmt wäre es viel schöner wenn man diesen Fortschritt direkt am Bild mitverfolgen könnte. In den nachfolgenden Teilaufgaben sollen Sie dies mit Hilfe von Threads realisieren und Multithreading anschließend dazu nutzen, dass Rendern der Bilder zu beschleunigen. WICHTIGER HINWEIS: Um die Reihenfolge der Erzeugung und Beendigung aller Threads auf der Konsole nachverfolgen zu können, muss bei allen Threads in dieser Aufgabe auf folgende Punkte geachtet werden: • Jeder Thread der erzeugt wird muss nach der Klasse benannt werden, deren run()-Methode im Thread ausgeführt wird. Werden mehrere Threads gleichen Typs erzeugt, müssen die Threads von 0 beginnend durchnummeriert werden, z.B. “RenderLineWorker 0” • Als erster Befehl in jeder run-Methode, soll auf der Konsole ausgegeben werden, dass der Thread mit dem entsprechenden Namen jetzt startet, z.B. “RenderWorker starts...”. • Als letzter Befehl in jeder run-Methode, soll auf der Konsole ausgegeben werden, dass der Thread mit dem entsprechenden Namen jetzt beendet wird, z.B. “RenderPoolWorker 2 ends...”. Falls diese Punkte bei den zu implementierenden Threads fehlen, führt dies zu einem Punktabzug bei den entsprechenden Teilaufgaben. RenderWorker (3 Punkte) Anstatt die Methode renderImage(...) direkt in der main-Methode von ThreadedRaytracer aufzurufen, soll die Erzeugung des Bildes in einen eigenen Thread ausgelagert werden. Erstellen Sie dafür eine Klasse RenderWorker, welche das Interface Runnable implementiert. Der Konstruktor soll eine Referenz auf die dazugehörige ThreadedRaytracer als Parameter erhalten und dieses in einer protected Member-Variablen speichern. Die Methode run() soll den Aufruf zum rendern des Bildes aus der main-Methode enthalten. Dort soll stattdessen ein Thread erzeugt und gestartet werden, welcher den RenderWorker ausführt. Sollten Sie das Programm nun ausführen, werden Sie feststellen, dass weiterhin keine Zwischenergebnisse angezeigt werden. Noch schlimmer: Auch im Anschluss an die Generierung des Bildes, wird das Bild nicht ohne weiteres im Fenster angezeigt. Das liegt daran, dass die Methode paint(), welche das Bild im Fenster anzeigt, lediglich ein einziges Mal beim Erstellen des Fensters aufgerufen 5 wird. Damit die Anzeige aktualisiert wird, muss das Fenster mit Hilfe der Methode repaint()dazu veranlasst werden. Eine Möglichkeit besteht darin, ein repaint im RenderWorker im Anschluss an die Berechnung des Bildes zu erzwingen. Dadurch werden allerdings nach wie vor keine Zwischenergebnisse angezeigt. Stattdessen sollen Sie als nächstes einen Thread schreiben, der in Abständen von 0,1 Sekunden das JFrame zu einer Aktualisierung veranlaßt. FrameRepainter (5 Punkte) Erstellen Sie dazu eine neue Klasse FrameRepainter, die vom Interface Runnable abgeleitet werden soll. Dem Konstruktor soll sowohl der ThreadedRaytracer, also auch der Thread mit dem RenderWorker übergeben werden. Beide Referenzen sollen in protected Member-Variablen gespeichert werden. In der Methode run() soll alle 0,1 Sekunden das Frame neu gezeichnet werden. Sobald der Thread mit dem RenderWorker beendet und das fertige Bild im Fenster angezeigt wird, oder vorher ein interrupt auftritt, soll sich der Thread von selbst beenden. Erstellen und starten Sie in der main-Methode einen Thread, der den FrameRepainter ausführt um das Bild fortwährend zu aktualisieren. Kontrollieren Sie, ob Sie den Fortschritt nun tatsächlich während dem Rendern mitverfolgen können. Multi-Threading beim Raytracing Bis hierhin wurden Threads nur dafür verwendet, dass der Fortschritt beim Raytracing auch tatsächlich im Fenster sichtbar ist. Darüber hinaus kann Multi-Threading auch dafür verwendet werden um die Berechnung des Bildes zu beschleunigen. Hierbei kann der Umstand genutzt werden, dass die Berechnung jedes einzelnen Pixels im Bild unabhängig voneinander möglich ist. Dadurch ist Raytracing ideal zur Parallelisierung geeignet. Der einzige kritische Abschnitt beim Raytracing besteht darin, wenn ein berechneter Farbwert tatsächlich in einem Pixel gesetzt wird. Da man grundsätzlich nicht davon ausgehen darf, dass das Setzen der einzelnen Pixel vollkommen unabhängig voneinander erfolgt, muss dieser Schreibzugriff synchronisiert werden. SynchronizedImage (2 Punkte) Erstellen Sie hierfür eine Klasse SynchronizedImage, welche im Konstruktor eine Referenz auf ein BufferedImage erhält und diese als protected Member-Variable speichert. Mit Hilfe der Methode writePixel(int x, int y, Color c) soll man in diesem Bild den entsprechenden Pixel an der Stelle (x,y) auf die Farbe c setzen können. Das Setzen des Pixels mit dieser Methode soll so synchronisiert sein, dass während dem Setzen von einem Pixel, keine weitere Pixel verändert werden können. ThreadedCamera Es gibt verschiedenste Möglichkeiten, auf welche Weise das Berechnen der Pixel auf Threads verteilt wird und wieviele Threads dabei verwendet werden. Unabhängig davon besitzen all diese Möglichkeiten sehr viele Gemeinsamkeiten, die in der abstrakten Klasse ThreadedCamera gekapselt sind. Eine Multithread-Kamera die von dieser Oberklasse abgeleitet ist, muss lediglich die Methode startRenderThreads() implementieren, welche die Threads zur Generierung des Bildes erstellt und startet. Wie in der Klasse ThreadedCamera, soll bei jeder abgeleiteten Klasse der field-of-view der Kamera durch den Konstruktor festgelegt werden. 6 Das tatsächliche Rendering wird mit Hilfe von Klassen realisiert, die vom Interface Runnable abegeleitet werden. Zum Berechnen der Farbwerte für einen einzelnen Pixel soll die Methode renderPixel in ThreadedCamera benutzt werden. Machen Sie sich mit dieser und den anderen Methoden der Klasse vertraut, in dem Sie die Kommentare lesen und den Code versuchen nachzuvollziehen. LinethreadCamera (5 Punkte) Eine triviale Idee zur Aufteilung des Renderings könnte darin bestehen, für jede einzelne Zeile im Bild einen eigenen Thread zu starten. Dieser Ansatz soll mit Hilfe der Klasse LinethreadCamera extends ThreadedCamera und der Klasse RenderLineWorker implements Runnable realisiert werden. Jede Instanz von RenderLineWorker soll im Konstruktor die dazugehörige LinethreadCamera sowie die Nummer der vom Thread zu berechnenden Zeile übergeben bekommen. In der Methode run() soll der entsprechende Thread die Pixel in dieser Zeile berechnen und sich danach von selbst beenden. Bei einem interrupt() soll der Thread keine weiteren Pixel rendern, sondern sich von selbst beenden. Die Threads für die einzelnen Zeilen sollen in der Methode startRenderThreads() erstellt und gestartet und als Liste zurückgegeben werden. Ersetzen Sie im ThreadedRaytracer die alte Kamera durch Ihre neu erstellte Kamera und starten Sie Ihr Programm. Vergleich der Rechenzeit (1 Punkt) In dem von Ihnen zu erweiterenden Projekt wird bereits die Zeit gemessen, die das Erstellen des Bildes in Anspruch nimmt. Wie viel Zeit benötigt das Rendern mit der ursprünglichen Camera und wie viel Zeit mit Ihrer neu erstellte LinethreadCamera? Notieren Sie in einem Kommentar die von Ihnen gemessenen Zeiten und welche Szene Sie für Ihren Test verwendet haben. Erklären Sie zusätzlich in ein bis drei Sätzen, wie dieser Unterschied zustande kommt. MultithreadCamera (5 Punkte) Wenn das Berechnen der Pixel möglichst gleichmäßig auf eine bestimmte Anzahl von Threads aufgeteilt werden soll, liegt es Nahe, dass jeder Thread die selbe Anzahl von Pixel berechnen soll. Dieser Ansatz soll mit Hilfe der Klasse MultithreadCamera extends ThreadedCamera und der Klasse RenderSameNumPixelWorker implements Runnable realisiert werden. Als Konstruktor soll die MultithreadCamera zusätzlich zum field-of-view die Anzahl der Threads übergeben bekommen, die zum Rendern genutzt werden sollen. Um die Pixel gleichmäßig auf die Threads aufteilen zu können, ist es einfacher wenn die Pixel mit einem Index statt mit Hilfe ihrer (x, y)-Koordinate addressiert werden können. Erstellen Sie in der Klasse MultithreadCamera dazu eine Methode renderPixel(int pixelIdx) die einen Pixel-Index automatisch in eine (x, y)Koordinate umgerechnet und den Farbwert für den entsprechenden Pixel bestimmt. Die Klasse RenderSameNumPixelWorker soll einen Konstruktor mit den Parametern MultithreadCamera camera, int firstPixel und int lastPixel zur Verfügung stellen. Die Methode run() soll dafür sorgen, dass alle Pixel mit einem Index i (f irstP ixel ≤ i < lastP ixel) berechnet werden1 . Wie bei der LinethreadCamera soll man die Render-Threads durch ein interrupt() unterbrechen können und in der Methode startRenderThreads() soll die entsprechende Anzahl der Threads 1 Mit anderen Worten ist firstPixel der erste Pixel der vom Thread gerendert wird und lastPixel ist der erste Pixel der nicht mehr von dem Thread berechnet wird. 7 erzeugt und jedem ein möglichst gleich großer Bereich von Pixeln übergeben werden. Testen Sie ihre neue Kamera und vergleichen Sie die Zeit zur Berechnung des Bildes mit den vorherigen Kamera-Implementierungen. ThreadpoolCamera (6 Punkte) Die MultithreadCamera versucht die Arbeitslast des Renderns dadurch möglichst gerecht zu verteilen, dass jeder Thread dieselbe Anzahl an Pixeln zu berechnen hat. Allerdings kann das Rendern von einzelnen Pixeln unterschiedlich aufwändig sein2 . Dies kann bei der MultihreadCamera dazu führen, dass einige Threads bereits beendet sind, während andere Threads weiterhin mit dem Rendern aufwändigerer Bereiche zu tun haben. Eine gerechtere Verteilung der Arbeitslast kann dadurch erreicht werden, dass das Bild nicht in fixe Bereiche pro Thread aufgeteilt wird, sondern dass das Bild in kleinere Bereich (z.B. Zeilen) zerlegt wird. Immer wenn ein Thread einen solchen Bereich erfolgreich bearbeitet hat, bekommt er den nächsten zu bearbeitenden Bereich zugeteilt, bis die gesamte Arbeitslast komplett auf die Threads aufgeteilt wurde. Diesen Ansatz sollen Sie mit Hilfe der Klassen ThreadpoolCamera extends ThreadedCamera und der Klasse RenderPoolWorker implements Runnable implementieren. Der Konstruktor der ThreadpoolCamerasoll wieder den field-of-view und die Anzahl der zu verwendenen Threads übergeben bekommen. Damit die Threads die Nummer der nächsten zu rendernden Zeile anfordern können, soll die KameraKlasse zusätzlich die Methode getNextLine() zur Verfügung stellen. Achten Sie darauf, dass zwei Threads nicht diesselbe Zeilennummer zugewiesen werden kann. Falls bereits alle Zeilen vergeben wurden, soll die Methode den Wert −1 zurückgeben. Die Klasse RenderPoolWorker benötigt im Konstruktor lediglich eine Referenz auf die dazugehörige ThreadpoolCamera. In der Methode run() soll jeder Thread fortwährend eine neue Zeilennummer anfordern und anschließend die entsprechenden Pixel berechnen. Sobald es keine zu rendernden Zeilen mehr gibt oder der Thread ein interrupt erhält, soll der Thread umgehend beendet werden. In der Methode startRenderThreads() soll wie üblich die entsprechende Anzahl der Threads erzeugt und gestartet werden. Testen Sie Ihre neue Klasse mit verschiedenen Szenen und einer unterschiedlichen Anzahl an Threads und beobachten Sie wie die Zeit zum Generieren des Bildes davon abhängt. 2 z.B. weil an spiegelnden Materialien sogenannte Sekundär-Strahlen rekursiv weiter verfolgt werden müssen. 8