Aufgabenblatt - 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, A. Lehmann, R. Fraedrich, F. Reichl
WS 2010
Aufgabenblatt 11
25. Januar – 31. Januar
Threads
11.1 (Ü) Allgemeine Fragen
(a) Was sind Threads und wozu dienen sie? Lassen Sie sich zur Veranschaulichung von Ihrem
Tutor das Rendern einer Grafik mit der bekannten Raytracer-Anwendung vorführen. Zunächst
mit einem einzigen, anschließend mit je einem Thread pro Pixel-Zeile.
(b) Aus mindestens wievielen Threads besteht jedes Programm?
(c) Was macht der folgende Java-Code? Wie und wo wird hier ein Thread erzeugt? Wann enden
beide Threads?
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 ());
t . start ();
}
}
(d) Was ist der Unterschied, wenn anstelle von t.start() die Methode t.run() aufgerufen
werden würde?
(e) Laufen Threads grundsätzlich parallel ab? Dürfen Sie Annahmen über die Laufzeit eines
Threads machen?
(f) Kann die Ausführung von Threads jederzeit, d.h. insbesondere an jeder Stelle, unterbrochen
werden? Betrachten Sie hierzu die Ausgabe des folgenden Beispielcodes:
public class VerboseThread implements Runnable {
private int id ;
public VerboseThread ( int id ) {
this . id = id ;
}
1
@Override
public void run () {
while (! Thread . interrupted ()) {
System . out . print ( " Thread " + id + " says : " );
System . out . println ( " I am thread " + id );
}
}
public static void main ( String [] args ) {
Thread tOne = new Thread ( new VerboseThread (1));
Thread tTwo = new Thread ( new VerboseThread (2));
tOne . start ();
tTwo . start ();
// ...
}
}
(g) Was machen die Methoden Thread.sleep(long millis), Thread.yield() und Thread.join()?
Wozu dient in diesem Zusammenhang eine InterruptedException?
(h) Sowohl Thread.sleep() als auch Thread.yield() sind statische Methoden. Auf welchen
Thread wirkt sich der Aufruf dieser Methoden aus? Betrachten Sie hierzu beispielsweise die
Methoden run() und main() aus Teilaufgabe (c).
(i) Diskutieren Sie den folgenden Code-Ausschnitt. Was macht dieses Programm? Vergegenwärtigen
Sie sich die verschiedenen Ausführungsstränge! Wie werden die Threads beendet?
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 ... " );
// Sleep for _about_ a second .
Thread . sleep (1000);
} catch ( InterruptedException e ) {
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 ();
try {
Thread . sleep (3000);
} catch ( InterruptedException e ) {
} finally {
// Tell the thread t to terminate .
t . interrupt ();
}
}
}
2
11.2 (Ü) 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
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 das Interface Runnable implementiert 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 ein entsprechendes Echo erzeugt wird. Wenn die Eingaben schnell genug hintereinander stattfinden, 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 werden, 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 sämtliche Echo-Threads vollständig ausgeführt wurden.
Ä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 Methode 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.
11.3 (Ü) Synchronisation
(a) Welche Werte können Threads untereinander lesen und schreiben?
3
(b) Worauf muss beim Zugriff auf gemeinsam genutzte Variablen und Objekte geachtet werden?
(c) Was bezeichnet der Begriff Race Condition? Nennen Sie Beispiele für Fehler, die in diesem
Zusammenhang 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 globalTicketCount = 10;
protected int ticketsToBuy ;
public TicketBooking ( int numTicketsToBuy ) {
ticketsToBuy = numTicketsToBuy ;
}
protected static synchronized boolean buyTickets ( int numTickets ) {
if ( globalTicketCount >= numTickets ) {
globalTicketCount -= numTickets ;
return true ;
}
return false ;
}
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 ( globalTicketCount ) {
if ( globalTicketCount >= ticketsToBuy ) {
globalTicketCount -= ticketsToBuy ;
} else {
System . out . println ( " Sorry , not enough tickets available ! " );
}
}
}
4
public static void main ( String [] args ) {
Thread t1 = new Thread ( new TicketBooking2 (4) );
Thread t2 = new Thread ( new TicketBooking2 (3) );
t1 . start ();
t2 . start ();
}
}
11.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 Mother und Child
an, die das Runnable-Interface implementieren. Beide Klassen erhalten in ihrem Konstruktor eine
Referenz auf denselben Stapel Cookies, welche sie in einer Membervariable speichern.
Beide Klassen enthalten in ihrer run()-Methode eine Endlosschleife. Die Mother-Klasse testet
dabei immer wieder, ob weniger als z.B. 30 Plätzchen vorhanden sind. Wenn dem so ist, erzeugt
sie ein neues Plätzchen und legt es auf den gemeinsamen Stapel. Die Child-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
einen Mutter- und drei Kinderthreads an, die alle Zugriff auf denselben 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.
5
11.5 (H) Threads beim Raytracing
(+++)
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.
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...“.
RenderWorker
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 den dazugehörigen ThreadedRaytracer als Parameter erhalten und diese 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, der dann 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
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 nachwievor 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
Erstellen Sie dazu eine neue Klasse FrameRepainter, die vom Interface Runnable abgeleitet werden soll. Dem Konstruktor soll sowohl eine Referenz auf den ThreadedRaytracer als auch eine
Referenz auf den Thread mit dem RenderWorker übergeben werden. Beide Referenzen sollen in
protected Member-Variablen gespeichert werden.
6
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 des Renderns 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, die
Berechnung des Bildes zu beschleunigen. Hierbei kann der Umstand genutzt werden, dass die Berechnung der 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
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 das entsprechende 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 des Setzen eines Pixels keine weiteren Pixel verändert werden
können.
ThreadedCamera
Es gibt verschiedenste Möglichkeiten, das Berechnen der Pixel auf Threads zu verteilen 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 Parameter
field-of-view der Kamera durch den Konstruktor festgelegt werden.
Das tatsächliche Rendering wird mit Hilfe von Klassen realisiert, die vom Interface Runnable abgeleitet werden. Zum Berechnen der Farbwerte eines einzelnen Pixels soll die Methode renderPixel()
in ThreadedCamera benutzt werden. Machen Sie sich mit dieser und den anderen Methoden der
Klasse vertraut, indem Sie die Kommentare lesen und den Code nachzuvollziehen versuchen.
LineThreadCamera
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 eine Referenz auf die dazugehörige
7
LineThreadCamera sowie die Nummer der vom Thread zu berechnenden Zeile übergeben bekommen. In der Methode run() soll der entsprechende Thread alle 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 von 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
In dem von Ihnen zu erweiternden Projekt wird bereits die Zeit gemessen, die das Erstellen des
Bildes in Anspruch nimmt. Wieviel Zeit benötigt das Rendern mit der ursprünglichen Camera und
wieviel Zeit mit Ihrer neu erstellten 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 zustandekommt.
MultiThreadCamera
Wenn das Berechnen der Pixel möglichst gleichmäßig auf eine bestimmte Anzahl von Threads
aufgeteilt werden soll, liegt es nahe, dass jeder Thread dieselbe Anzahl von Pixeln 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 das entsprechende 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
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
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 MultiThreadCamera dazu führen, dass einige Threads bereits beendet sind, während andere Threads weiterhin mit dem
Rendern aufwändigerer Bereiche zu tun haben.
1
Mit anderen Worten ist firstPixel das erste Pixel, der vom Thread gerendert wird und lastPixel das erste
Pixel, der nicht mehr vom Thread berechnet wird.
2
Zum Beispiel, weil an spiegelnden Materialien sogenannte Sekundär-Strahlen rekursiv weiter verfolgt werden
müssen.
8
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
ThreadPoolCamera soll 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 niemals dieselbe 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.
9
Herunterladen