parallele tiefensuche in java

Werbung
Fachbereich Elektrotechnik & Informatik
PARALLELE TIEFENSUCHE IN JAVA
MÖGLICHKEITEN UND GRENZEN
Kai Oberbeckmann
∗
Eingereicht am 7. September 2014
Hausarbeit im Rahmen der Veranstaltung
PARALLELE PROGRAMMIERUNG und VERTEILTE
SYSTEME
von Prof. Dr. rer. nat. Jörg Frochte und Prof. Dr.-Ing. Ludwig
Schwoerer
∗ [email protected]
Copyright und Bildquellen
Text: Copyright (c) 2014 KAI OBERBECKMANN
Bilder und Skizze: Copyright (c) 2014 KAI OBERBECKMANN
Es ist Ihnen gestattet diese Arbeit zu vervielfältigen, zu verbreiten und öffentlich zugänglich
zu machen sofern Sie folgende Bedingungen einhalten:
• Namensnennung: Sie müssen die Urheberschaft ausreichend deutlich benennen.
Diese Angaben dürfen in jeder angemessenen Art und Weise gemacht werden, allerdings nicht so, dass der Eindruck entsteht, der Lizenzgeber unterstütze gerade Sie
oder Ihre Nutzung des Werks besonders.
• Keine kommerzielle Nutzung: Sie dürfen das Material nicht für kommerzielle
Zwecke nutzen.
• Keine Bearbeitung: Wenn Sie das Material remixen, verändern oder darauf anderweitig direkt aufbauen, dürfen Sie die bearbeitete Fassung des Materials nicht
verbreiten.
Abweichende Lizenzen für einzelnen Bilder und Skizzen werden ggf. separat angegeben. Es
wurde jedoch darauf geachtet, dass keine dieser Lizenzen die Möglichkeiten der Rechte im
obigen Sinne einschränken.
Codebeispiele dürfen unter der BSD-3-Clause
http://opensource.org/licenses/BSD-3-Clause
verwendet und weitergegeben werden.
Eidesstattliche Erklärung
Ich versichere, dass ich die Arbeit selbständig verfasst und keinen als die angegebenen
Quellen und Hilfsmittel benutzt sowie Zitate kenntlich gemacht habe.
Die Regelungen der geltenden Prüfungsordnung zu Versäumnis, Rücktritt, Täuschung und
Ordnungsverstoß habe ich zur Kenntnis genommen.
Diese Arbeit hat in gleicher oder ähnlicher Form keiner Prüfungsbehörde vorgelegen.
Haan, den
Unterschrift
1
Inhaltsverzeichnis
1 Einleitung
2 Grundlagen
2.1 Graphen . . . . . . . . . . . . . . .
2.1.1 Bäume . . . . . . . . . . . .
2.2 Suchverfahren . . . . . . . . . . . .
2.2.1 Allgemein: Suchalgorithmen
2.2.2 Tiefensuche . . . . . . . . .
2.2.3 Parallele Tiefensuche . . . .
3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4
4
6
7
7
9
12
3 Parallele Tiefensuche in Java: Implementierung
3.1 Vorarbeit . . . . . . . . . . . . . . . . . . . .
3.1.1 Aufgabenverteilung . . . . . . . . . . .
3.1.2 Basiswissen: Parallelisieren in Java . .
3.2 Implementierung . . . . . . . . . . . . . . . .
3.2.1 Tiefensuche in Java . . . . . . . . . . .
3.2.2 Tiefensuche-Thread . . . . . . . . . . .
3.2.3 Zusammenführung der Prozesse . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
15
15
15
15
17
17
18
18
4 Ergebnisse
4.1 Problemtik bei der Parallelisierung der Tiefensuche . . . . . . . . . . . . .
4.2 Laufzeitanalyse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.1 Analyse: Verhalten von Java . . . . . . . . . . . . . . . . . . . . . .
21
21
21
22
5 Fazit
25
2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1 Einleitung
Die hier vorliegende Arbeit befasst sich mit einem sehr zentralen Thema der modernen
Informationstechnologie, der Suche nach Objekten. Seit digitale Daten an Bedeutung gewonnen haben, ist eine automatische Verarbeitung essentiell geworden. Die Informationen
sollen für die einzelnen Systeme und letztlich für den Menschen nutzbar gemacht werden.
Hierzu muss ein Datensatz gefunden werden, um dessen Inhalt wiedergeben zu können.
Folglich muss in einer endlichen Datenmenge ein vordefiniertes Objekt möglichst schnell
und Ressourcen sparend gefunden werden. In großen Datenmengen, wie sie auf Servern
und PCs aber auch auf mobilen Endgeräten zu finden sind, ist ein effektives Vorgehen
gefragt, um den Aufwand in Grenzen zu halten und Zeit oder ggf. Energie zu sparen.
Zu Beginn muss die heutige Software-Architektur betrachtet werden. Sie baut auf die Objektorientierung auf, welche Datenstrukturen zu etwas Greifbarem macht und Bezüge unter
den Elementen herstellt. Dies beschleunigt sowohl die Entwicklung als auch die Wartung
von Software und vermeidet Fehler bei der Implementierung.
Ein solches Netz an Informationen, sei es hierarchisch oder von anderer Beschaffenheit, lässt
sich durch sogenannte Graphen darstellen. Diese visualisieren Objektbezüge und müssen
auf der Suche nach dem Zieldatensatz durchkämmt werden. Anschauliche Beispiele hierfür
sind U-Bahn- und Straßennetze. Wenn ein bestimmtes Element gesucht wird, soll dieses
mit möglichst wenigen Schritten gefunden werden. Zu diesem Zweck gibt es zahlreichen
Ansätze zu denen auch die Breiten- und die Tiefensuche zählen.
Letztere soll in dieser Arbeit im Bezug auf ihre Parallelisierbarkeit hin geprüft werden.
Es soll erforscht werden inwiefern eine Aufteilung der Tiefensuche auf mehrere Prozesse
einen Geschwindigkeitsvorteil bietet, wie groß dieser ist und wie er beeinflusst werden
kann. Hierzu wird eine Implementierung in Java vorgenommen und untersucht wie weit
sich diese Programmiersprache zu solch einem Zweck eignet und was beim Umgang mit
dieser zu beachten ist.
3
2 Grundlagen
2.1 Graphen
Grundsätzlich verkörpern Graphen Datenstrukturen. Anwendung finden diese vor allem bei
Sortier- und Suchverfahren, Sprachverarbeitung und Speicherverwaltung (vgl. [Ger96]).
Faktisch sind die Objekte, welche in Graphen als Knoten bezeichnet werden, die Datensätze. Diese können direkt oder über andere Knoten zusammenhängen oder auch keine
Verbindung zueinander haben. Ein Graph ist stark zusammenhängend, wenn von jedem
Knoten alle anderen erreicht werden können. Die Verknüpfungen hierzu werden Kanten
genannt. Diese bestimmen, ob ein Graph gerichtet oder ungerichtet ist. Sind alle Kanten
in beide Richtungen durchlaufbar handelt es sich um einen ungerichteten Graphen, wie er
in Abbildung 2.1 gezeigt wird. Sollte dies nicht der Fall sein, spricht man von einem gerichteten Graphen (s. Abbildung 2.2), wie es bei einer Einbahnstraße auf der Straßenkarte
der Fall wäre.
Abbildung 2.1: Beispiel eines ungerichteten Graphen, Quelle: yEd Graph Editor
4
Abbildung 2.2: Beispiel eines gerichteten Graphen, Quelle: yEd Graph Editor
Kanten können sowohl ein- (sogenannte Schlingen, vgl. [Ger96]) als auch zweielementig
sein. Bei gerichteten Graphen können so maximale und minimale Knoten entstehen. Ein
maximaler Knoten besitzt keinen Nachfolger (s. Abbildung 2.2 Knoten 1). Alle Kanten
zeigen also in seine Richtung. Ein minimaler Knoten ist genau der umgekehrte Fall und
ist in Abbildung 2.2 bei Knoten 10 zu finden.
Zum Abbilden eines einfachen Graphen in einer Datenstruktur können Inzidenz- oder
Adjazenzmatrizen verwendet werden. Bei einer Inzidenzmatrix wird jeder Kante und jedem
Knoten eine fortlaufend Zahl zugeordnet. Im Falle einer Adjazenzmatrix werden nur die
Knoten nummeriert. In der Matrix kann so vermerkt werden welches Element mit einem
anderen verbunden ist. Auch die Richtung der Verbindung kann so registriert werden.
Inzidenzmatrizen setzen Knoten mit den verbundenen Kanten ins Verhältnis (vgl. (2.2)).
Bei Adjazenzmatrizen geschieht dies mit zwei Knoten (vgl. (2.1)). Druch das Einsparen
der Nummerierung der Kanten wird die Matrix schlussendlich kleiner.

0

1

0


1

0
Adjazenzmatrix = 
0


0

0


1
0
0
0
1
0
0
0
0
0
0
1
0
1
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
1
0
0
0
1
0
0
0
0
1
0
0
1
1
0
0
1
0
0
0
0
0
1
0
1
0
0
0
0
0
0
0
0
0
1
0
0
0
0
0
0
0
0
0
0

0

0

0

0


0

0


0

0

0

0
(2.1)
5

+1

+1

+1


 0

 0

 0


 0

 0


0
Inzidenzmatrix = 

 0

 0


 0

 0


 0

 0


 0
0
T
−1 0
0
0
0
0
0
0
0

0
0
0
0
0
0
0 −1 0 

0
0 −1 0
0
0
0
0
0

0
0 +1 0
0
0
0 −1 0 


0
0
0
0 +1 0
0 −1 0 

−1 0
0
0
0 +1 0
0
0


+1 0
0
0
0
0
0
0 −1

+1 −1 0
0
0
0
0
0
0

−1 +1 0
0
0
0
0
0
0


0
0
0
0
0 +1 −1 0
0

0
0
0
0
0 +1 0
0 −1


0
0
0
0 +1 0 −1 0
0

0
0
0
0
0
0 +1 0 −1


0 +1 0
0
0
0
0
0 −1

0
0
0 −1 +1 0
0
0
0

0
0
0 +1 −1 0
0
0
0

0
0
0 +1 0
0
0
0 −1
(2.2)
Die Benennung der Matrizen rührt daher, dass Knoten, welche eine gemeinsame Kante besitzen, als adjazenz bezeichnet werden. Hingegen werden Kanten mit einem gemeinsamen
Knoten oder Kanten und Knoten, bei denen der Knoten einen Endpunkt der Kante stellt,
„inzident“ genannt (vgl. [Kä99]).
Neben dieser Darstellungsart ist außerdem eine Darstellung mit verketteten Listen denkbar. Solche Listen benötigen weniger Speicherplatz, jedoch können sie bei den Zugriffszeiten
nicht bei den Matrizen mithalten.
2.1.1 Bäume
Bäume sind ein Sonderfall eines Graphen. Aufgrund ihrer „Verästelung“ kann eine stark
hierarchische Struktur abgebildet werden. Zudem sind alle Bäume kreisfrei und zusammenhängend. Kreisfrei bedeutet, dass es keinen Weg über den Graphen gibt, welcher zum
Ausgangspunkt wieder zurückführt.
Sollte es nur einen minimalen Knoten im Baum geben, so handelt es sich um einen Wurzelbaum. Die maximalen Knoten sind dessen Blätter. Der Grad eines Knotens gibt bei
einem Baum die Anzahl der Verzweigungen an, die dieser hat. Ein vollständiger k-Baum
ist folglich einer, dessen Knoten, ausgenommen der Blätter, alle einen Grad k bieten. Ein
unvollständiger Baum ist in Abbildung 2.3 zu finden. Ein weiterer Sonderfall von Bäumen
sind die binären Bäume, welche an all ihren Knoten einen maximalen Grad von 3 besitzen.
6
Abbildung 2.3: Beispiel eines unvollständigen ungerichteten 4-Wurzelbaumes, Quelle: yEd
Graph Editor
2.2 Suchverfahren
2.2.1 Allgemein: Suchalgorithmen
Eine Suche definiert sich als „Selektieren von Elementen mit einer vorgegebenen Eigenschaft über einer ungeordneten oder geordneten Datenmenge. Durch eine bestimmte Ordnung kann die Suche deutlich effektiver durchgeführt werden“ [Ger96]. Somit ist ein Vorgehen/ Algorithmus zu entwickeln, welcher diese Aufgabe möglichst schnell mit dem Gebrauch weniger Ressourcen ausführt. Hierzu gibt es zunächst einige Ansätze.
Handelt es sich bei der zu durchsuchenden Datenmenge um eine geordnete, wird die Suche
erleichtert und es kann bspw. ein binäres Suchen angewandt werden. Es wird iterativ oder
rekursiv ein Teil der Datenmenge ausgeschlossen und so eine geringe Komplexität erreicht,
wie sie in (2.3) gezeigt wird.
Komplexität = O(log2 n)
(2.3)
Anders Verhalten sich Suchalgorithmen auf unsortierten bzw. nicht nach der gesuchten
Eigenschaft sortierten Datenmengen. Ein lineares Suchverfahren ist gefragt, welches jedes
einzelne Element überprüft, bis das entsprechende gefunden wurde. Die gemittelte Komplexität in diesem Fall stellt sich in (2.4) dar.
n
(2.4)
Komplexität = O( )
2
Breiten- und Tiefensuche gehören zu dieser Familie. Je nach Suchmenge terminiert der
7
Algorithmus früher oder später. Der Aufwand skaliert jedoch mit der Anzahl der zu durchsuchenden Objekt, also mit der Größe des Graphen.
Über dies hinaus gibt es die sogenannte Mustersuche, welche nach Mustern wie beispielsweise Strings suchen. Hierauf möchte ich im Rahmen dieser Arbeit jedoch nicht eingehen.
Begrifflichkeiten
Eine Suche auf einem Graphen kann in mehrere Mengen aufgeteilt werden. Zunächst existiert die Menge V0 (Engl.: „vertex“, z. Dt: „Knoten“), die ausschließlich den Startknoten
enthalten. Darüber hinaus wird die Menge der Kanten Et (Engl.: „edge“, z. Dt. „Kante“)
und der Knoten Vt genutzt, die die bereits besuchten Elemente enthält (vgl. [Kä99]). Die
Teilmengen, welche sich aus E\Et und V \Vt ergeben, gelten als unbesucht.
Als aktive Knoten gelten solche, die inzident zu einer unbesuchten Kante liegen und in Vt
enthalten sind. Andere Knoten, die in Vt enthalten sind, diese Anforderung jedoch nicht
erfüllen, sind vollkommen abgearbeitet.
Der aktuelle Knoten ist der zum Zeitpunkt t untersuchte Knoten, von dem die nächste
Aktivität ausgeht.
Ablauf
Nach [Kä99] lässt sich eine Graphensuche in zwei Schritte aufteilen:
1. Falls noch nicht alle Kanten besucht sind, wird ein aktiver Knoten ausgewählt. Hierzu
bedarf es einer Knotenauswahlregel.
2. Aus E\Et wird eine Kante ausgesucht und vom aktuellen Punkt auslaufend orientiert.
Es wird eine Kantenauswahlregel angewandt.
Eine solche Suche kann folglich von drei Komponenten verkörpert werden:
1. Kantencontainer M
2. Knotenauswahl ν
3. Kantenauswahl α
Ein solcher Ablauf ist in Abbildung 2.4 aufgezeigt. Em ist hier die zuletzt besuchte Kante,
wenn die Tiefensuche beendet wird.
8
Abbildung 2.4: Grundsätzlicher Verlauf einer Graphensuche unabhängig vom Algorithmus
(vgl. [Kä99]), Quelle: yEd Graph Editor
2.2.2 Tiefensuche
Grundidee
Die Tiefensuche möchte den Graphen vorrangig in der „Tiefe“ untersuchen. Es wird unter
Berücksichtigung der Kanten- und Knotenauswahlregel immer weiter fortgeschritten bis
ein Knoten keine unbesuchten adjazenten Knoten mehr aufweist. In diesem Fall wird ein
Schritt zurück zum vorher besuchten Knoten gemacht und von diesem aus weitergearbeitet.
Die Kantenauswahlregel kann sich je nach Implementation des Algorithmus unterscheiden
und sich an verschiedenen Parametern der Objekte orientieren. Für die Knotenauswahlregel ist jedoch der Tiefensuchealgorithmus selbst zuständig. Anders als beispielsweise bei
der Breitensuche, welche versucht die aktiven Knoten möglichst auf gleichem Abstand zum
Ausgangsknoten zu halten, wählt die Tiefensuche soweit möglich den am weitesten entfernten Knoten als nächsten aktuellen Knoten. In dieser Manier kann lediglich im Falle
eines notwendigen Rückschrittes nicht gearbeitet werden.
Mit diesem Algorithmus entsteht nach [SK00] schlussendlich für den besuchten Teilgraphen
ein Wald aus Wurzelbäumen. Solange es sich um einen zusammenhängenden Graphen handelt, wird zudem jedes Element erreicht.
Der Algorithmus terminiert, sobald er das Element mit der gesuchten Eigenschaft gefunden hat oder keine „Arbeit“ mehr hat. Dieser Fall tritt ein, falls die Tiefensuche erneut
am Ausgangsknoten (oder bei der parallelen Ausführung beim letzten Element des Stacks)
angekommen ist und auch dieser keine unbesuchten adjazenten Knoten mehr besitzt. Diese
Art der Tiefensuche wird Knoten-Tiefensuche genannt und findet in dieser Arbeit Anwendung. Neben dieser existiert auch eine Kanten-Tiefensuche, welche so lange fortschreitet,
9
wie eine unbesuchte Kante vorhanden ist.
Algorithmus
Es existieren verschiedene Varianten des Tiefensuche-Algorithmus. Von diesen soll hier
jedoch lediglich die Knoten-Tiefensuche betrachtet werden. Sie bietet ein effektives Vorgehen, da kein Datensatz/ Objekt mehrmals besucht werden kann. Somit besitzt diese
Ausführung einen Geschwindigkeitsvorteil gegenüber der Kanten-Tiefensuche.
Grundsätzliches Vorgehen nach [Kä99]:
Knotenauswahl
1. Endknoten der Kante, welche zuletzt in M eingefügt wurde („tiefster“ Knoten)
Kantenauswahl
1. Falls v aktiv,
2.
wähle eine unbesuchte und zu v inzidente Kante v, w und orientiere sie (v, w)
3.
falls w unbesucht
4.
lege (v, w) in M ab
5. ansonsten
6.
10
entferne (u, v) aus
Der Algorithmus ist, wie folgt:
Algorithmus 1 : Ausführung einer Knoten-Tiefensuche
Data : V0 = {v}, GesuchteEigenschaf t, Vt = {}, Et = {}, M = {}, G
Result : Vm
1 while v hat nicht die gesuchte Eigenschaft & V \Vt 6= {} do
2
while v ist aktiv & v hat nicht die gesuchte Eigenschaft do
3
wähle eine eine Kante (v, w) aus E\Et
4
Vt = Vt ∪ {v}
5
Et = Et ∪ {(v, w)}
6
if w 6∈ Vt then
7
M = M ∪ {(v, w)}
8
v←w
9
end
10
end
11 end
12 if v hat die gesuchte Eigenschaft then
13
gib v aus
14 else
15
Element wurde nicht gefunden
16 end
Bei dem in Algorithmus 1 aufgezeigten Vorgehen können je nach Beschaffenheit des Graphen und nach Position des gesuchten Objektes große Laufzeitunterschiede auftreten. Statistisch kann jedoch ermittelt werden, dass die Suche, wie andere solcher Suchverfahren
auch, linear mit der Anzahl der Knoten skaliert (vgl. (2.4)).
Diese Auswahl einer noch unbesuchten Kante vom aktuellen Knoten aus, ist ein Punkt, den
jede Implementierung anders handhaben kann. Es kann nach einer Nummerierung oder
gleich zufällig entschieden werden. Auf die Laufzeit sollte dies bei unsortierten Graphen
keinen Einfluss haben.
Um das Durchlaufen von Kreisen und zu langen Wege zu vermeiden, definiert man sogenannte Cutoff-Tiefen (vgl. [VK88]). Diese begrenzen die maximale Länge eines Weges
durch den Graphen, bis ein Rückschritt erfolgen muss. Diese maximale Tiefe muss mindestens gleich dem „Durchmesser“ des Graphen entsprechen, damit mit Sicherheit das
gesuchte Element gefunden wird. Andernfalls könnte ein Teilgraph ausgelassen werden.
Der IDA*-Suchalgorithmus verwendet hier keinen statischen Wert, sondern passt diesen
nach Bedarf an.
Iterative-Deepening-A*-Search
Die IDA*-Suche ist ein „kostenorientierter“ Algorithmus. Grundlagen dieses Algorithmus
ist die Möglichkeit einer Abschätzung der „Kosten“ einen bestimmten Knoten n zu erreichen und von diesem aus zum gesuchten Objekt zu gelangen. Zu diesem Zweck werden
11
drei Funktionen definiert (nach [VK87]):
• g(n): Stellt die „Kosten“ für das Erreichen des Knotens n vom Ausgangspunkt dar
• h(n): Ist eine Abschätzung für die „Kosten“ von n an den Knoten mit der gesuchten
Eigenschaft zu kommen
• f (n) = g(n) + h(n): Summe der „Kosten“
Während der Durchführung führt ein Überschreiten des Schwellwertes zu einem Rückschritt. Bei jedem Schritt wird der Schwellwert neu gebildet.
2.2.3 Parallele Tiefensuche
Um die Rechenzeit herunter zu setzen, sollen in der Informatik Mehrkernprozessoren mit
ebenfalls mehreren Threads genutzt werden. Dazu ist der einzelne Algorithmus genau zu
betrachten, um herauszufinden, wie dieser mit mehreren Prozessen gleichzeitig abgehandelt werden kann. Anders als beim Durchsuchen von einfachen Gebilden wie beispielsweise
Listen kann die „Arbeit“ im Fall des Durchsuchens eines Graphen nicht von vorne hinein
unter den Prozessen geteilt werden, sodass jeder Prozess seinen Teil autark abhandeln kann.
Bei der Parallelisierung der Tiefensuche ist ein aufwändigeres Vorgehen gefragt, da der
Graph in seiner Komplexität nicht zwangsweise von Anfang an bekannt ist. Er muss im
Laufe der Tiefensuche „erkundet“ werden und eine Möglichkeit gefunden werden, die „Arbeit“ trotzdem sinnvoll aufzuteilen. Aufgrund dessen zählt die Aufgabenteilung zu einem
der entscheidenden Faktor bei einer erfolgreichen Implementierung der parallelen Tiefensuche.
Ein weiterer bedeutsamer Parameter mit Blick auf die Effektivität und Geschwindigkeit
des Algorithmus ist offensichtlich die Beschaffenheit des Graphen. Zunächst dauert eine
Suche durch einen großen Graphen länger als die durch einen kleinen. Darüber hinaus ist
allerdings auch die Anordnung der Kanten entscheidend. Es gibt Graphen, welche sich für
die Parallelisierung besser eignen als andere, weil sie beispielsweise eine gewisse Symmetrie
aufweisen und weniger Kommunikation zwischen den einzelnen Prozessoren erforderlich ist.
Was zu einer zusätzlichen einflussreichen Größe führt, die Geschwindigkeit der Kommunikation zwischen den Prozessen. Je nach Anzahl der Kommunkationsvorgänge zwischen den
Prozessen kann die benötigte Zeit einen nicht zu vernachlässigenden Teil der Rechenzeit
ausmachen.
Aufgabenverteilung
Um die genannte „Arbeit“ zu verteilen, muss eine passende Architektur gewählt werden.
Dies kann in der Software oder auch direkt in der Hardware geschehen. Hier kämen beispielsweise 1-Ring-, 2-Ring-, Hypercube oder Shared-Memory-Architekturen in Betracht.
12
Die letzten beiden sind nach [VK88] zu bevorzugen und weisen deutliche PerformanceVorteile auf.
Die genannten Architekturen unterscheiden sich wie folgt:
• 1-Ring: Es kann nur mit einem anderen Nachbar kommuniziert werden
• 2-Ring: Von beiden Nachbarn kann „Arbeit“ abgerufen werden
• Hypercube: Eine Anfrage kann an log(N ) der N Prozesse gestellt werden
• Shared-Memory: Alle Threads stehen für Anfragen offen
Ersichtlich ist, dass bei einer höheren Anzahl von Prozessen auf die zugegriffen werden
kann weniger Schritte nötig sind, um die „Arbeit“ sinnvoll zu verteilen. Dies bildet einen
großen Vorteil der Shared-Memory-Variante.
Außerdem sind zwei weitere Aspekte zu betrachten, um eine sinnvolle Aufgabenverteilung
zu implementieren. In dem Moment, in dem die „Arbeit“ geteilt wird, muss entschieden
werden, welcher Teil beim ursprünglichen Prozess verbleibt und welcher dem anfragenden
Prozess übergeben wird. Diesbezüglich wurde in [VK88] festgestellt, dass ein Halbieren die
effektivste Lösung darstellt. Beim Abweichen in die eine oder andere Richtung wird einer
der Prozesse zu schnell ohne „Arbeit“ sein. Dies gilt jedoch nur für Shared-Memory- oder
Hypercube-Architekturen. Bei diesen können anschließen alle anderen Prozesse auf beide
zugreifen und nach „Arbeit“ fragen. Ist dies nicht der Fall, wie bei den Ringarchitekturen,
ist eine andere Herangehensweise gefragt, welche beispielsweise mehr „Arbeit“ an den
nächsten Prozess weiterreicht, um die Zahl der Kommunikationsvorgänge gering zu halten.
Laufzeitbetrachtung, Effizienz und Isoefficiency Funktionen
Die Laufzeit ist im Vorhinein nur statistisch abzuschätzen. Wenn der gesamte Graph durchsucht werden muss, um erfolgreich zu sein, kann die Geschwindigkeit durch die Parallelisierung bei N Prozesse maximal um den Faktor N zunehmen. Sollte dies jedoch nicht der
Fall sein und die parallele Suche weniger Schritte zum finden des Elementes brauchen als
die Suche mit einem einzelnen Prozess, kann der Faktor sogar größer als N werden (vgl.
[VK87]). Diesen Effekt nennt man „Accelaration Anomaly“ (z. Dt.: „Beschleunigungsanomalie“).
Bei der Parallelisierung muss außerdem bei ungefähr gleichschnellen Prozessen nicht mit
einer Verschlechterung der Laufzeit gerechnet werden. Die Zeiten für die Kommunikation
zwischen den Prozessen sind hierbei vernachlässigbar (vgl. [VK87]).
Um zu beurteilen wie „gut“ der Algorithmus in seiner parallelen Form arbeitet, berechnet
man seine Effizienz. Diese lässt sich aus der Anzahl der Prozessoren und dem Laufzeitunterschied errechnen. Wenn die Laufzeit mit dem Faktor N abnimmt, handelt es sich um
13
eine Suche mit einer Effizienz von 1. Bei allen Werten darüber oder darunter errechnet sich
der Wert prozentual. Dabei ist zu beachten, dass diese Zahl stark von der Kommunikationszeit zwischen den Prozessen beeinflusst wird. Aus diesem Grund nimmt die Effizienz
mit zunehmender Prozesszahl ab. Steigert man allerdings zusätzlich auch die Größe des
Graphen und so die zur Verfügung stehende „Arbeit“ , so kann die Effizienz auf dem gleichen Level gehalten werden.
Die sich daraus ergebende Funktion nennt sich „Isoefficiency function“ (vgl.[VK87]). Sie
kann je nach Prozessorarchitektur unterschiedliche Verläufe annehmen:
• 1-Ring: Abhängig vom Aufteilungsverhältnis, tendenziell exponentiell
• 2-Ring: Ähnliches Verhalten wie die 1-Ring-Architektur
• Hypercube: Funktion ist polynomisch (Grad hängt vom Aufteilungsverhältnis ab)
• Shared-Memory - Jeder Prozess fragt bei allen anderen nach „Arbeit“ , falls er welche
benötigt
– Ein logarithmischer Faktor trennt von der Linearität:
W = O(Uc omm · V (N ) · log(V (N ))
– V (N ) ≥ N : Anzahl der Abfrage bis zu der jeder Prozessor mindestens einmal
abgefragt wurde
• Enhanced-Shared-Memory - Ein Prozess fragt immer nur bei einem anderen nach
„Arbeit“. Sollte er dort keine „Arbeit“ erhalten, fragt er den nächsten an.
– Funktion weist ebenfalls ein nahezu lineares Verhalten auf (Faktor leicht verbessert):
W = O(N 2 · log(N )
14
3 Parallele Tiefensuche in Java: Implementierung
Der Kernpunkt dieser Arbeit ist die Parallelisierung der Tiefensuche in Java und die anschließende Analyse der Ergebnisse hinsichtlich der Umsetzbarkeit, der Performance und
der Wahl der Programmiersprache. Dies möchte ich in den nun folgenden Abschnitten
anhand meiner eigenen Implementierung erläutern.
3.1 Vorarbeit
3.1.1 Aufgabenverteilung
Da die verwendete Programmiersprache ohnehin auf einer virtuellen Maschine läuft und
folglich alles andere als hardwarenah ist, sind der Wahl des Algorithmus für die Aufgabenverteilung von Seiten der Architektur keine Grenzen gesetzt. Aus diesem Grund habe ich
mich für das schnellste bisher vorgestellte Vorgehen (Enhanced-Shared-Memory) entschieden.
Wie zuvor genannt, muss dabei eine globale Variable etabliert werden, welche den als
nächstes anzufragenden Prozess bestimmt. Sollte jeder Prozess selbst entscheiden, wann er
eine neue Anfrage nach „Arbeit“ stellt, können viele simultane Zugriffe auf einen einzelnen
Prozess auftreten. Dies kann zu Konflikten führen, die aufwändig gelöst werden müssten,
um alle Prozesse gleichmäßig zu bedienen. Deshalb wird dieser Implementierung der Parallelisierung eine andere Herangehensweise gewählt. Es soll ein weiterer Prozess hinzugefügt
werden, welcher eine organisierende Funktion einnimmt. Dieser soll der selbe Strang sein,
der für das Starten und Initialisieren der anderen Prozesse zuständig ist. Dieser ist somit
ohnehin notwendig. Die von ihm übernommenen Punkte sind die Initialisierung, die Aufgabenverteilung und die Einleitung der Terminierung aller Prozesse.
Zu Beginn der Suche erhält der erste Prozess den gesamten Graphen. Es sind somit einige
Umschichtungsprozesse erforderlich, um eine gleichmäßige Verteilung der „Arbeit“ zu
erreichen. Die Aufgaben werden in Form eines Stapels (Stacks) aus Knoten verwaltet.
Dieser beinhaltet den bisher abgelaufenen Weg. Im Falle einer Anfrage gibt ein Prozess
einen Teil dieses Stapels (Größe ist abhängig vom definierten Aufteilungsverhältnis) an
einen anderen weiter. Außerdem sollen die schon besuchten Knoten übermittelt werden,
um Mehrfachbesuche zu vermeiden und die Zeit für die Suche zu verkürzen.
3.1.2 Basiswissen: Parallelisieren in Java
Wie man es von modernen Hochsprachen gewohnt ist stellt Java Unmengen an Funktionen in seinem API zur Verfügung. So auch für das Implementieren von Threads. Hierfür
15
kann ein Thread-Object erzeugt oder von diesem abgeleitet werden, um die sich ergebenen
Funktionen zu nutzen. Um zu bestimmen welche Aufgaben auf dem neu erzeugten Thread
ausgeführt werden, kann beim Aufruf des Konstruktors ein Runnable übergeben werden
(s. Listing 3.1) oder gleich die run-Methode des Thread-Objektes implementiert werden.
Nach dem Aufruf der start-Methode wird der hier eingefügt Code anschließend sequenziell
abgearbeitet. Da für die Tiefensuche Anweisung vielmals wiederholt werden, muss hier mit
Schleifen hantiert werden.
Listing 3.1: Erstellen und Starten eines Threads in Java
1 Runnable myRunnable = new Runnable () {
2
3
@Override
4
public void run () {
5
doSomeWork ();
6
}
7 };
8
9 Thread myThread = new Thread ( myRunnable );
10 myThread . start ();
Interessant ist beim Multithreading mit Java jedoch die Synchronisierung und die Terminierung von Threads. Ein synchrones Verhalten zweier Stränge kann vor allem durch
Wartezeiten erreicht werden. Ein Thread wartet auf ein Ereignis. Sollte dieses eintreten,
läuft er weiter. In Java gibt es die Methoden wait und join, welche das Warten auf ein
Benachrichtigung eines anderen Threads bzw. auf die Terminierung eines anderen Strangs
warten. Für die Benachrichtigung können die Methoden notify und notifyAll genutzt werden (s. Listing 3.2). Wie diese Funktion in diesem Projekt verwendet wurde, soll später
näher erläutert werden.
Listing 3.2: Thread-Synchronisierung in Java
1 Thread 1:
2 synchronized ( notifier ) {
3
try {
4
notifier . wait ();
5
} catch ( I n t e r r u p t e d E x c e p t i o n e ) {
6
e . printStackTrace ();
7
}
8 }
9
10 Thread 2:
11 synchronized ( notifier ) {
12
notifier . notify ();
13 }
16
3.2 Implementierung
3.2.1 Tiefensuche in Java
Um die Tiefensuche umzusetzen, mussten zunächst die einzelnen Bestandteile als Datentypen abgebildet werden. Vor allem der Graph sowie die schon öfter genannte „Arbeit“ können in der objektorientierten Umgebung gut dargestellt werden. Für den Graph kam eine
Implementation der Princeton University zum Einsatz. Durch das Erben von dieser Klasse und Nutzen ihrer Funktionen wurde erreicht, dass ein binärer Baum beliebiger Größe
erzeugt werden kann. Dieser Baum wird hier in den Laufzeittests verwendet, da das Verhalten des Algorithmus einfach nachvollziehbar ist. Im Falle der „Arbeit“ ist eine eigene
Lösung gefunden worden, welche lediglich einen Stack mit den Knoten des bisher zurückgelegten Weges und ein Set mit den schon besuchten Knoten enthält. Mit Kenntnis des
Graphen, welcher später jedem Prozess übergeben wird, kann jeder Thread mit diesen
Informationen die Tiefensuche durchführen.
Nachdem die DFS mit Graph, Arbeit, Zielknoten und Cutoff-Tiefe initialisiert wurde, kann
sie schrittweise ihre Suche vollführen. Für den nächsten Schritt ist immer ein Aufruf seitens
des zuständigen Threads nötig. Ein einzelner Schritt ist in Abbildung 3.1 dargestellt. Es
wird die zuvor beschriebene herkömmliche Tiefensuche verwendet.
Abbildung 3.1: Ablauf eines Schrittes der Tiefensuche in der Java Implementation, Quelle:
Libre Office Draw
Falls eine Aufgabenteilung stattfindet und der Stack geteilt wird, muss das Objekt nicht
neu erzeugt werden. Es wird der entsprechende Teil des Stack entfernt und die Verarbeitung kann anschließend weitergehen. Das Gleiche gilt für den Fall, wenn „Arbeit“ für die
17
Tiefensuche bereitgestellt wird. Ohne Neuerzeugen kann diese eingefügt werden.
3.2.2 Tiefensuche-Thread
Ein einzelner Thread, der die Suche abarbeitet, bildet einen einzelnen Prozess im Gesamtprogramm. Damit der organisierende Strang sinnvoll auf den Tiefensuche-Thread zugreifen
kann und dieser die Suche autark durchführt, müssen noch weitere Funktionalitäten bereitgestellt werden.
Zunächst muss der Thread eine Schleife ausführen, welche wiederholt die Tiefensuche aufruft (s. Abbildung 3.2). Dies sollte solange geschehen bis er das entsprechende Element
findet, aus irgendeinem Grund vom organisierenden Thread unterbrochen wird oder keine
Arbeit mehr hat. Ein Grund für die Unterbrechung durch den übergeordneten Thread wäre
das Finden des Elementes durch einen anderen Prozess. Die Arbeit geht dem Strang aus,
sobald der Stack der Tiefensuche leer ist. Um dies zu beobachten, muss der Zustand des
Stack in jedem Zyklus abgefragt werden.
Sollte keine Arbeit vorliegen, wird die Schleife beendet und der Thread fällt in einen
Wartemodus. Dort verweilt er bis er entweder unterbrochen wird oder Arbeit vom organisierenden Thread bereitgestellt wird (und die entsprechende Benachrichtigung an den
Thread gerichtet wird). Dann könnte die Arbeit unverzüglich wieder aufgenommen werden.
Eine weitere benötigte Funktion ist das Sperren (Engl.: locking) des Strangs. Falls Arbeit von einem anderen Prozess benötigt und aufgrund dessen angefragt wird, darf der
angefragte Thread im Zeitraum des Abrufs keine Änderungen mehr an der „Arbeitsmenge“ vornehmen. Hierzu wird der Strang für die erforderliche Zeit angehalten. Sobald der
Vorgang abgeschlossen ist, kann der Thread ungehindert weiterarbeiten.
3.2.3 Zusammenführung der Prozesse
Um einen reibungslosen Ablauf der Tiefensuche in mehreren separaten Prozessen zu gewährleisten, bedarf es einem Strang, welcher organisatorische Funktionen übernimmt. Dieser wird als Erstes gestartet und terminiert als Letztes. Der Thread hat drei Hauptaufgaben, die Initialisierung der Prozesse, die Arbeitsverteilung und die Terminierung aller
Threads (s. Abbildung 3.3).
Als aller Erstes müssen die Stränge erstellt und initialisiert werden. Hierzu werden DFSTdObjekte (Td=Thread) erzeugt, denen durchgängig der Graph und der Zielknoten zur Verfügung gestellt wird. Nur der erste Prozess erhält allerdings „Arbeit“ in Form eines Stacks,
welcher ausschließlich mit dem Startknoten gefüllt ist. Nach dem Start des Strang wird von
hier aus die Suche begonnen. Die anderen Prozesse fallen unverzüglich in den Wartemodus
bis ihnen „Arbeit“ bereitgestellt wird.
18
Abbildung 3.2: Ablauf des Threads zur Steuerung der Tiefensuche in der Java Implementation, Quelle: Libre Office Draw
Darauf folgend übernimmt der übergeordnete Strang eine überwachende Funktion ein. Es
werden zwei Parameter eines jeden Threads beobachtet. Zum einen wird jeder zyklisch
abgefragt, ob er das gesuchte Element gefunden hat. In diesem Fall kann die Tiefensuche
sofort abgebrochen werden. Alle Threads werden beendet und das Ergebnis inklusive der
benötigten Zeit wird ausgegeben. Den anderen Parameter stellt das Vorhandensein von
„Arbeit“ dar. Nacheinander werden die Threads auf diesen Zustand hin überprüft. Sollte
ein Prozess keine „Arbeit“ mehr besitzen, startet der organisierende Thread die Suche
danach. Hierfür wurde zuerst eine Variable gespeichert, welche den Index des nächsten
Prozesses enthält, der „Arbeit“ bereitstellen soll. Diese Variable hilft dabei alle Prozesse
gleichmäßig stark mit Anfragen nach „Arbeit“ zu belasten und somit dessen Suche zu
verlangsamen. Folglich wird der entsprechende Prozess nach „Arbeit“ gefragt. Im Falle
einer Ablehnung wird der Index inkrementiert und der nächste Prozess zu Rate gezogen.
Dieser Vorgang wird so lange fortgesetzt bis Arbeit von einem Prozess erlangt werden
konnte oder alle anderen Prozess erfolglos abgefragt wurden. Anschließend wird überprüft,
ob der nächste Prozess in der Reihe „Arbeit“ benötigt.
Der übergeordnete Prozess terminiert folglich, falls das passende Element gefunden wurde
oder aber wenn alle Suchthreads nacheinander abgefragt wurde und sie weder selbst „Arbeit“ besaßen noch selbiges von den anderen erhalten konnten. In diesen Fällen wird der
Nutzer anschließend mittels einer Textausgabe darüber informiert, ob die Suche erfolgreich
war oder fehlgeschlagen ist, da das gesuchte Element nicht gefunden werden konnte.
19
Abbildung 3.3: Ablauf des übergeordneten Threads in der Java Implementation, Quelle:
Libre Office Draw
20
4 Ergebnisse
4.1 Problemtik bei der Parallelisierung der Tiefensuche
Das Parallelsieren des Tiefensuche-Algorithmus stellt an sich keine einfache Aufgabe dar.
Der zu durchsuchende Graph kann sehr schwierig in gleichgroße „Arbeitspakete“ aufgeteilt
werden, da dieser zunächst „erkundet“ werden muss. Nur so könnte jedoch die Kommunikation zwischen den Threads so klein wie möglich gehalten werden. Auch wenn das gewählte Verfahren eines der effektivsten für diesen Zweck ist, werden durch dessen Anwendung
viele Umschichtungsprozesse notwendig. Jeder einzelne verlangsamt den Suchvorgang. Die
Zahl der nötigen Kommunikationsvorgänge hängt nicht nur vom Aufteilungsverhältnis des
Stacks ab, sondern auch sehr bedeutend von der Beschaffenheit des Graphen. Hierfür ist
der hier implementierte binäre Baum ein Vertreter, der sehr viele Umschichtungsprozesse
verursacht, da der untere Teil des Stacks, welcher bei einer Anfrage weitergegeben wird,
eine um ein Vielfaches größere „Arbeitsmenge“ aufweist als der restliche Teil.
Wie zuvor beschrieben werden die Stränge bei einer Anfrage nach „Arbeit“ gesperrt,
um unvorhersehbare Ereignisse zu vermeiden. So können diese Threads zu dieser Zeit ihre Suche nicht fortsetzen. Sollten nun aus den genannten Gründen viele Umschichtungen
notwendig sein oder zu einem späteren Zeitpunkt ohnehin wenig „Arbeit“ übrig sein, häufen sich die Anfragen. Falls jede sofort behandelt werden würde, käme schlussendlich der
Suchvorgang zum Erliegen, da ein Thread mit „Arbeit“ fast ausschließlich gesperrt wäre.
Als Konsequenz muss eine Wartezeit eingefügt werden, welche die Anzahl der Anfragen
pro Sekunde begrenzt. So kann überhaupt erst ein Fortschritt in der Tiefensuche erzielt
werden.
4.2 Laufzeitanalyse
Der Zweck der Parallelsierung der Tiefensuche ist die Verbesserung der Laufzeit, welche
sich möglichst linear zu Anzahl der Prozessoren verhalten sollte. Wie zuvor erwähnt, kann
mit dem verwendeten Verfahren ein solch lineares Verhalten nicht erzielt werden, jedoch
ist eine Annäherung daran möglich.
Bei durchgeführten Laufzeittests konnte jedoch keine signifikante Verbesserung durch das
Erhöhen der Anzahl der Prozesse erreicht werden. Die benötigte Zeit für einen Suchvorgang
blieb nahezu identisch. Zumal die Rechenzeit wohl möglich aufgrund anderer Prozesse auf
dem Computer ohnehin schwankte und ein einzelner Wert schwer zu ermitteln war. Durch
Mitteln der erreichten Zeiten konnte jedoch ein Wert errechnet werden, der als Richtwert
21
gesehen werden kann. Auf Gründe für dieses ungünstige Verhalten wird im nächsten Abschnitt eingegangen.
Starke Unterschiede bezüglich der Laufzeit konnten allerdings bei variierender Größe des
Baumes beobachtet werden. Hier kam es zu einem nachvollziehbaren Verhalten. Beim Zusammenhang zwischen Laufzeit und Knotenanzahl kann ein lineares Verhalten erkannt
werden (s. Abbildung 4.1). Da dies für bis zu 15 Prozesse gilt (maximal getestete Anzahl), kann davon ausgegangen werden, dass der Einfluss der Kommunkationsvorgänge
vernachlässigbar ist.
Abbildung 4.1: Gemessener Zusammenhang zwischen der Anzahl der Knoten des binären
Baumes und der Laufzeit, Quelle: Libre Office Calc
Falls die Laufzeit hingegen gegenüber der Höhe des durchsuchten Baumes aufgetragen
wird, kommt es zu einem quadratischen Verlauf der Funktion (s. Abbildung 4.2). Dies ist
ebenfalls verständlich, da sich die Anzahl der Knoten mit jeder weiteren Stufe des Baumes
verdoppelt und so auch doppelt so viele Schritte nötig sind, um den ganzen Baum zu
durchsuchen.
4.2.1 Analyse: Verhalten von Java
Java ist grundsätzlich keine hardwarenahe Sprache. Der generierte Bytecode wird auf der
JVM, einer virtuellen Maschine, ausgeführt. Alle Ressourcen werden von dieser verwaltet.
22
Abbildung 4.2: Gemessener Zusammenhang zwischen der Höhe des binären Baumes und
der Laufzeit, Quelle: Libre Office Calc
Um es dem Programmierer einfacher zu gestaltet Software zu schreiben, beinhaltet diese
virtuelle Maschine auch einen Garbage Collect (kurz GC), welcher auf einem separaten
Thread ausgeführt wird und für die Rückgewinnung von Speicher zuständig ist. Somit laufen beim Start einer Java Anwendung automatisch mehrere Threads, welche anschließend
vom darunterliegenden Betriebssystem verwaltet werden.
Zusätzliche Stränge entstehen durch das Nutzen von Objekte, welche nicht selbst implementiert wurden. Dabei können teilweise zusätzliche Threads eröffnet werden. Solche Objekte werden auch in der hier vorgestellten Implementierung verwendet. Aus diesem Grund
läuft auch eine Tiefensuche mit nur einem Thread auf allen 8 Hyperthreads des Testrechners. Alle Ressourcen werden somit ausgeschöpft, da die Prozessorkerne vollständig
ausgelastet werden. Die Idee hinter der parallelen Implementierung ist hingegen, die ungenutzte Rechenleistung der Kerne, welche bei einer Implementierung mit einem einzigen
Thread inaktiv sind, zu erschließen und so den Prozess zu beschleunigen. Wenn nun jedoch
keine ungenutzten Ressourcen mehr zur Verfügung stehen, führt das Erzeugen mehrerer
Threads für die Durchführung der Tiefensuche eher zu einer Verlangsamung des Vorgangs,
da zusätzlich zum Ablauf der Tiefensuche noch die Kommunikationsvorgänge abzuarbeiten
sind. Somit sollte eine Ausführung mit mehreren Prozessen theoretisch betrachtet langsa-
23
mer sein als eine mit einem einzelnen. Offenbar sind die Kommunikationsschritte jedoch
zu vernachlässigen und schlagen sich nicht in den Laufzeiten nieder.
Als Lösungsansatz kann ein anderes Vorgehen bei der Implementierung des Algorithmus
gesehen werden. Es sollten möglichst nur primitive Datentypen benutzt werden, um die
Verwendung von Threads überschaubarer zu machen. Die Verwendung von Inzidenz- oder
Adjazenzmatrizen ist denkbar. Das Erstellen einer solchen Ausführung der parallelen Tiefensuche würde jedoch einen deutlich höheren Aufwand bedeuten. Aufgrund dessen sollte
in Erwägung gezogen werden, eine anderen Programmiersprache, welche hardwarenäher
ist, zu verwenden, um so das Verhalten der Tiefensuche besser beurteilen zu können.
Leider konnte mit dem hier geschriebenen Programm keine Verbesserung durch die Verwendung von Multithreading bezüglich der Geschwindigkeit erzielt werden.
24
5 Fazit
In der hier vorliegenden Arbeit konnten einige Aspekte der parallelen Tiefensuche näher beleuchtet werden. Zunächst ist eine Struktur erstellt worden die einzelnen Elemente
der Tiefensuche objektorientiert abbildet und so für eine Suche nutzbar macht. Anschließend wurden passende Programmteile in Java implementiert. Hierbei wurde Wert auf eine
schnelle Suche gelegt, die sparend mit den Ressourcen des Rechners umgeht˙
Die spätere Untersuchung des Programms brachte keine Verbesserung durch die Parallelisierung ans Licht. Dies ist mit großer Wahrscheinlichkeit auf die genannten Problem mit
zusätzlich entstehenden Threads zurückzuführen. Demnach bedarf es hier einer sorgsamen
Überarbeitung der Implementierung zurück auf einfache Datentypen, um mehr Kontrolle
zu erhalten. Andernfalls sollte festgehalten werden, dass Java für solch einen Zweck nicht
die Sprache der ersten Wahl ist.
Nichtsdestotrotz ist das Ergebnis dieser Arbeit eine funktionierende Tiefensuche, welche
auch große Graphen schnell durchsuchen kann und zuverlässig arbeitet. Die Laufzeiten,
welche bei unterschiedlich großen Graphen auftraten, zeigten zusätzlich das erwartete Verhalten. Die benötigte Zeit bewegte sich linear zur Anzahl der Knoten des durchsuchten
Baumes.
25
Literaturverzeichnis
[Ger96] Gerber, S.: Grundkurs Praktische Informatik. Vorlesung, 1996
[Kä99] Kääb, V.: Dualität von Suchstrategien auf planaren Graphen, Universität Konstanz, Diplomarbeit, 1999
[SK00] S. Krumke, H. W. H. Noltemeier N. H. Noltemeier: Graphentheoretische Konzepte und Algorithmen / Konrad-Zuse-Zentrum für Informationstechnik Berlin.
2000 (1). – Forschungsbericht
[VK87] V. Kumar, V. Nageshwara R.: Parallel Depth First Search, Part I: Implementation / University of Texas at Austin, Department of Computer Science. 1987 (1).
– Forschungsbericht
[VK88] V. Kumar, V. Nageshwara R.: Parallel Depth First Search, Part II: Analysis
/ University of Texas at Austin, Department of Computer Science. 1988 (2). –
Forschungsbericht
26
Abbildungsverzeichnis
2.1
2.2
2.3
2.4
3.1
3.2
3.3
4.1
4.2
Beispiel eines ungerichteten Graphen, Quelle: yEd Graph Editor . . . . . .
Beispiel eines gerichteten Graphen, Quelle: yEd Graph Editor . . . . . . .
Beispiel eines unvollständigen ungerichteten 4-Wurzelbaumes, Quelle: yEd
Graph Editor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Grundsätzlicher Verlauf einer Graphensuche unabhängig vom Algorithmus
(vgl. [Kä99]), Quelle: yEd Graph Editor . . . . . . . . . . . . . . . . . . .
Ablauf eines Schrittes der Tiefensuche in der Java Implementation, Quelle:
Libre Office Draw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ablauf des Threads zur Steuerung der Tiefensuche in der Java Implementation, Quelle: Libre Office Draw . . . . . . . . . . . . . . . . . . . . . . .
Ablauf des übergeordneten Threads in der Java Implementation, Quelle:
Libre Office Draw . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Gemessener Zusammenhang zwischen der Anzahl der Knoten des binären
Baumes und der Laufzeit, Quelle: Libre Office Calc . . . . . . . . . . . . .
Gemessener Zusammenhang zwischen der Höhe des binären Baumes und
der Laufzeit, Quelle: Libre Office Calc . . . . . . . . . . . . . . . . . . . . .
4
5
7
9
17
19
20
22
23
27
Liste der Algorithmen
1
28
Ausführung einer Knoten-Tiefensuche . . . . . . . . . . . . . . . . . . . . .
11
Listings
3.1
3.2
Erstellen und Starten eines Threads in Java . . . . . . . . . . . . . . . . .
Thread-Synchronisierung in Java . . . . . . . . . . . . . . . . . . . . . . .
16
16
29
Herunterladen