Übungsblatt 12 - TUM - Technische Universität München

Werbung
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
Zugehörige Unterlagen
Herunterladen