Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Fachhochschule Köln University of Applied Sciences Cologne 09 Fachbereich Nachrichtentechnik Institut für Informatik Diplomarbeit Synchronisation und Kommunikation von Prozessen und Threads in Java, Windows NT 4.0 und Unix / Linux Student : Stefan Krumbach Matr. Nr.: 11012566 11 Referent : Prof. Dr. C. Vogt Korreferent : Abgabedatum : Prof. Dr. D. Rosenthal November 2001 Hiermit versichere ich, daß ich die Diplomarbeit selbständig angefertigt und keine anderen als die angegebenen und bei Zitaten kenntlich gemachten Quellen und Hilfsmittel benutzt habe. ( Stefan Krumbach ) 2 Inhaltsverzeichnis 1 Einleitung ......................................................................................................................7 1.1 Inhalte dieser Diplomarbeit .................................................................................8 1.2 Synchronisation von Prozessen / Threads..........................................................9 1.3 Kommunikation zwischen Prozessen / Threads..................................................9 1.4 Deadlock ..........................................................................................................10 2 Programmiersprache Java.........................................................................................11 2.1 Grundlegende Eigenschaften ...........................................................................11 2.2 Prozesse / Threads in Java ..............................................................................12 2.3 Zustände von Threads......................................................................................12 2.4 Priorität und Scheduling....................................................................................14 2.5 Dämon-Threads................................................................................................14 2.6 Thread Gruppen ...............................................................................................15 2.7 Erzeugung und Steuerung von Threads ...........................................................16 2.8 Synchronisation ................................................................................................20 2.8.1 Synchronisation durch Priorität .............................................................20 2.8.2 Synchronisation durch Monitore ...........................................................20 2.8.3 Synchronisation durch Sperr-Objekte ...................................................21 2.8.4 Synchronisation durch Klassenbezogene-Sperre .................................23 2.8.5 Synchronisation durch Ereignisse.........................................................25 2.8.6 Synchronisation auf das Ende von anderen Threads............................26 2.9 Kommunikation zwischen Threads mit Piped-Streams .....................................28 3 Betriebssytem Windows NT 4.0.................................................................................32 3.1 Grundlegende Eigenschaften ...........................................................................32 3.2 Prozesse / Threads in Windows NT..................................................................32 3.3 Zustände von Prozessen / Threads ..................................................................33 3.4 Scheduling........................................................................................................34 3.5 Erzeugung und Steuerung von Prozessen / Threads........................................35 3.6 Synchronisation von Threads ...........................................................................41 3.6.1 Synchronisation durch Priorität .............................................................41 3.6.2 Synchronisation durch kritische Bereiche .............................................41 3.6.3 Synchronisation durch Ereignisse.........................................................43 3.6.4 Synchronisation durch Mutex................................................................49 3.6.5 Synchronisation durch Semaphore .......................................................51 3 3.7 Kommunikation.................................................................................................53 3.7.1 Unbenannte Pipes ................................................................................55 3.7.2 Benannte Pipes ....................................................................................56 4 Betriebssystem Unix / Linux......................................................................................62 4.1 Grundlegende Eigenschaften ...........................................................................62 4.2 Prozesse / Threads in Unix / Linux ...................................................................62 4.3 Zustände von Prozessen ..................................................................................63 4.4 Priorität und Scheduling....................................................................................64 4.5 Erzeugung und Steuerung von Prozessen........................................................65 4.6 Hintergrundprozesse "Dämons"........................................................................69 4.7 Synchronisation ................................................................................................69 4.7.1 Synchronisation durch Semaphore .......................................................69 4.7.2 Synchronisation durch Signale .............................................................73 4.7.3 Wechselseitiger Ausschluß mit Spinlocks .............................................75 4.8 Prozesskommunikation.....................................................................................76 4.8.1 Pipes ....................................................................................................76 4.8.2 Message Queues .................................................................................81 4.8.3 Shared Memory ....................................................................................82 4.8.4 Sockets ................................................................................................82 4.8.5 Streams................................................................................................84 5 Beispiel Applet............................................................................................................85 5.1 Grundlegende Arbeitsweise eines Applets........................................................85 5.2 Eigenschaften des Beispiel Applets ..................................................................86 5.3 Klassen des Applets .........................................................................................89 5.3.1 Die Klasse Applet1.java........................................................................89 5.3.2 Die Klasse Beenden.java......................................................................93 5.3.3 Die Klasse Counter.java .......................................................................94 5.3.4 Die Klasse CountingThread.java ..........................................................94 5.3.5 Die Klasse CountingThreadRunnable.java ...........................................96 5.3.6 Die Klasse ImageCanvas.java ..............................................................97 5.3.7 Die Klasse IntJTextField.java................................................................98 5.3.8 Die Klasse MeineTextArea.java............................................................99 5.3.9 Die Klasse Steuerung.java .................................................................100 5.3.10 Die Klasse Syncprio.java ..................................................................101 4 6 Beispielprogramme ..................................................................................................102 6.1 Beispielprogramme für Java ...........................................................................103 6.1.1 Counter ..............................................................................................103 6.1.2 Thread................................................................................................104 6.1.3 Zwei Threads......................................................................................106 6.1.4 Priorität...............................................................................................108 6.1.5 Monitore .............................................................................................111 6.1.6 Sperr-Objekt .......................................................................................114 6.1.7 Ereignis ..............................................................................................118 6.1.8 Reihenfolge ........................................................................................122 6.1.9 Pipe ....................................................................................................125 6.2 Beispielprogramme für Windows NT...............................................................128 6.2.1 Counter ..............................................................................................128 6.2.2 Thread................................................................................................129 6.2.3 Zwei Threads......................................................................................131 6.2.4 Priorität...............................................................................................133 6.2.5 Ereignis ..............................................................................................136 6.2.6 Semaphor...........................................................................................139 6.2.7 Philosophen .......................................................................................142 6.2.8 Ereignis für zwei Prozesse .................................................................145 6.2.9 Unbenannte Pipe................................................................................147 6.2.10 Benannte Pipe eine Richtung ...........................................................150 6.2.11 Benannte Pipe zwei Richtungen .......................................................154 6.2.12 Benannte Pipe für zwei Prozesse .....................................................159 6.3 Beispielprogramme für Unix / Linux ................................................................163 6.3.1 Counter ..............................................................................................163 6.3.2 Prozess ..............................................................................................164 6.3.3 Zwei Prozesse ....................................................................................165 6.3.4 Semaphor...........................................................................................167 6.3.5 Signale ...............................................................................................170 6.3.6 Unbenannte Pipe................................................................................172 6.3.7 Benannte Pipe für zwei getrennte Prozesse .......................................174 7 Abschlussbetrachtung.............................................................................................177 5 8 Anhang ......................................................................................................................178 8.1 Literaturverzeichnis.........................................................................................178 8.2 Internetquellen................................................................................................178 8.3 Werkzeuge .....................................................................................................179 8.4 Source-Code des Beispiel Applets..................................................................180 8.4.1 Die Klasse Applet1.java......................................................................180 8.4.2 Die Klasse Beenden.java....................................................................198 8.4.3 Die Klasse Counter.java .....................................................................199 8.4.4 Die Klasse CountingThread.java ........................................................200 8.4.5 Die Klasse CountingThreadRunnable.java .........................................201 8.4.6 Die Klasse ImageCanvas.java ............................................................203 8.4.7 Die Klasse IntJTextField.java..............................................................204 8.4.8 Die Klasse MeineTextArea.java..........................................................205 8.4.9 Die Klasse Steuerung.java .................................................................207 8.4.10 Die Klasse Syncprio.java ..................................................................208 8.4.11 Implementierung des Beispiel Applets in HTML-Seite.......................209 6 1 Einleitung 1 Einleitung Heutige Betriebssysteme und die Programmiersprache Java erlauben es Programmcode von einer oder mehreren Anwendung(en) in mehrere Teile zu unterteilen und diese Teile "parallel" auszuführen. Diese nebenläufigen Programme / Programmteile werden Prozesse oder Threads genannt. Der Begriff "parallel" wird in dieser Diplomarbeit häufiger verwendet. Er ist aber nur zum Teil richtig, da echte Nebenläufigkeit (parallele Bearbeitung von Prozessen / Threads) nur in einem System mit mehreren Prozessoren stattfinden kann. Bei einem System mit einem Prozessor spricht man von einer Pseudo-Nebenläufigkeit. Hierbei können die Prozesse / Threads nicht parallel ausgeführt werden. Für den Anwender scheint es, als würden die Prozesse / Threads parallel ausgeführt, weil der Prozessor schnell zwischen den einzelnen Prozessen / Threads umgeschaltet wird. Diese Diplomarbeit erläutert die Synchronisation und Kommunikation von diesen Prozessen / Threads in verschiedenen Umgebungen (Betriebssystem Unix / Linux, Windows NT und der Programmiersprache Java). Es ist notwendig genau zu unterscheiden was die einzelnen Betriebssysteme bzw. die Programmiersprache unter diesen Begriffen verstehen und wie sie umgesetzt werden. Die Art und Weise, wie die benötigten Funktionen bzw. Methoden in den einzelnen Umgebungen erläutert werden, richtet sich nach den Fachbüchern der jeweiligen Umgebung. Im allgemeinen werden die Begriffe Prozess und Thread wie folgt definiert: • Prozess (engl. process oder task) ist ein Kontext zusammen mit dem Programmcode. • Thread oder Lightweight Process (leichtgewichtiger Prozess) ist eine Aktivität (= Programmausführung) innerhalb eines Prozesses. Eine genaue Erklärung, wie die einzelnen Umgebungen die Begriffe Prozess / Thread umsetzen, findet in dem jeweiligen Kapitel der Umgebung statt. Weitere Begriffe, die in diesem Zusammenhang oft genannt und daher hier kurz erläutert werden, sind: • Multitasking Beim Multitasking wird ein Programm in mehrere Ausführungseinheiten unterteilt. Diese Ausführungseinheiten werden z.B. in Unix / Linux Systemen Kind-Prozesse genannt und als eigenständige Prozesse behandelt. • Multithreading Bei Java und Windows NT kann ein Programm (Prozess) auch in mehrere Ausführungseinheiten unterteilt werden. Diese Ausführungseinheiten (Threads) sind für das jeweilige System einfacher zu verwalten. 7 1 Einleitung • Multiprogramming Beim Multiprogramming ist es möglich mehrere eigenständige Programme "parallel" auszuführen. • Multiprocessing Besitzt ein System mehr als einen Prozessor spricht man von einem Multprocessing bzw. Multiprozessorsystem. 1.1 Inhalte dieser Diplomarbeit Kapitel 1 beinhaltet außer der Einleitung und dieser Inhaltsangabe eine kurze allgemeine Erläuterung von Synchronisation, Kommunikation und dem Begriff Deadlock im Zusammenhang mit Prozessen / Threads. Kapitel 2 zeigt, wie die Synchronisation und Kommunikation von Threads bei der Programmiersprache Java realisiert wird. Kapitel 3 veranschaulicht wie in Windows NT 4.0 Prozesse und Threads synchronisiert werden können. Außerdem wird die Kommunikation mit Hilfe von Pipes erläutert. Kapitel 4 geht auf die Synchronisation und Kommunikation von Prozessen in dem Betriebssystem Unix bzw. Linux ein. Des weiteren wird auf die unterschiedlichen Kommunikationsmöglichkeiten der Prozesse eingegangen. Im Vordergrund steht aber auch in diesem Kapitel die Kommunikation mit Pipes. Kapitel 5 erläutert ein Applet, daß für die HTML-Version dieser Diplomarbeit programmiert wurde. Dieses Applet ermöglicht dem Anwender die Auswirkungen der einzelnen Synchronisationsmechanismen auszuprobieren. Kapitel 6 beinhaltet einfache Beispielprogramme für Java, Windows NT und Unix bzw. Linux. Bei diesen Beispielprogrammen wurde der Schwerpunkt auf die Synchronisation und Kommunikation der Prozesse und Threads gelegt. Kapitel 7 zeigt eine Abschlussbetrachtung. Anhang Im Anhang ist das Literaturverzeichnis, die angewendeten Werkzeuge und der Source-Code des Applets abgedruckt. 8 1 Einleitung 1.2 Synchronisation von Prozessen / Threads Der eigentliche Sinn von seperaten Prozessen / Threads innerhalb einer Anwendung liegt darin, daß verschiedene Aufgaben unabhängig von einander bearbeitet werden. In einigen Fällen müssen die einzelnen Prozesse / Threads aber auch aufeinander abgestimmt ( synchronisiert ) werden, z.B. wenn mehrere Prozesse / Threads sich ein Betriebsmittel teilen oder ein Prozess / Thread auf das Ergebnis eines anderen Prozesses / Threads warten muß. Diese Synchronisation kann auf verschiedene Arten erfolgen. Jedes der Betriebssysteme bzw. die Programmiersprache Java bietet verschiedene Mechanismen zur Synchronisation an, die später genauer erläutert werden. Allgemein unterscheidet man bei der Synchronisation zwei Hauptarten: 1. Wechselseitiger Ausschluss Bei dieser Art der Synchronisation wird sichergestellt, daß eine Ressource nur von einem Prozess / Thread benutzt wird. Alle anderen Prozesse / Threads müssen warten, bis die Ressource wieder freigegeben wird. 2. Reihenfolgebedingung Bei dieser Art der Synchronisation wird die Reihenfolge festgelegt, in der die einzelnen Prozesse / Threads abgearbeitet werden. 1.3 Kommunikation zwischen Prozessen / Threads Obwohl Prozesse / Threads unabhängig von einander sind müssen sie Daten untereinander austauschen können. Dies ist z.B. nötig, wenn ein Prozess / Thread mit dem Ergebnis eines andern Prozesses / Threads arbeiten muß. Die einzelnen Umgebungen bieten verschiedene Methoden an diese Kommunikation zu ermöglichen. Eine Möglichkeit, die vom Grundprinzip in allen Umgebungen sehr ähnlich ist, ist die Kommunikation mit Hilfe von Pipes. Aus diesem Grund wird diese Art der Kommunikation zwischen verschiedenen Prozessen / Threads genauer erläutert. Wie die Kommunikation mit Pipes in den einzelnen Umgebungen implementiert ist wird in dem jeweiligen Kapitel der Umgebung genauer erklärt. 9 1 Einleitung 1.4 Deadlock Ein Deadlock (offizielle deutsche Übersetzung: "Verklemmung") entsteht, wenn mehrere Prozesse / Threads sich gegenseitig so beeinflussen das keiner von ihnen in der Lage ist seine Aufgabe bis zum Ende auszuführen. Eine solche Behinderung entsteht besonders schnell, wenn die Prozesse / Threads mehrere Ressourcen gleichzeitig benötigen. Die Aufgabe der Synchronisation ist es einen solchen Deadlock zu verhindern. Abb. 1.1: Das Philosophen-Problem [MiSi1999] Um einen Deadlock anschaulich zu erläutern verwendet man in der Grundlagenliteratur meist das Beispiel der denkenden und essenden Philosophen. Mehrere Philosophen verbringen ihre Zeit mit Denken und Essen. Sie sitzen an einem runden Tisch, jeder Philosoph hat einen Teller vor sich, und zwischen den Tellern liegt eine Gabel. Wenn ein Philosoph essen will, benötigt er zwei Gabeln. Da er sich jedoch beide mit seinem Nachbarn teilen muß, können nie zwei benachbart sitzende Philosophen gleichzeitig essen. Während die Philosophen denken, sind sie ausschließlich mit sich selbst beschäftigt und stehen nicht in Kontakt mit ihrer Umwelt. Wenn sie jedoch vom vielen Denken hungrig geworden sind, müssen sie versuchen, zwei Gabeln zu bekommen, um dann zu essen. Damit das Essen gerecht verteilt wird und kein Philosoph verhungert, muß man sich eine Regelung für die Vergabe des Bestecks einfallen lassen. Man könnte auf die Idee kommen, daß jeder Philosoph zunächst das Besteck zu seiner Rechten ergreift, dieses also in Besitz nimmt, und dann darauf wartet, daß das entsprechende andere Teil zu seiner Linken frei ist. Wenn dabei zufällig alle Philosophen gleichzeitig hungrig werden, dann nimmt jeder das Teil zu seiner Rechten in Besitz. Damit hält jeder eine Gabel, und auf dem Tisch ist kein Besteck mehr übrig. Alle Philosophen warten gegenseitig darauf, daß ein anderer sein Besteck wieder ablegt. In diesem Zustand würden alle Philosophen ewig warten und so verhungern. Man nennt diesen Zustand Deadlock. [MiSi1999] 10 2 Programmiersprache Java 2 Programmiersprache Java 2.1 Grundlegende Eigenschaften Java wurde von der Firma Sun entwickelt und erstmals am 23. Mai 1995 als neue, objektorientierte und plattformunabhängige Programmiersprache vorgestellt. Durch die Portabilität ist Java ideal für das Internet. Kleine Java Programme, die sogenannten Applets, können durch ein eigens dafür definiertes "Tag" in HTML-Seiten eingebunden werden und dadurch die statischen HTML- Seiten mit Leben erfüllen. Heutzutage werden immer mehr Internetanwendungen mit Hilfe von Applets umgesetzt. Einige Eigenschaften von Java sind: Portabel Java ist portabel. Dies wird dadurch erreicht, daß eine virtuelle Maschine, die für alle heutigen Betriebssysteme erhältlich ist, den kompilierten Bytecode der Java Anwendungen ausführt. Objektorientiert Java ist vollständig objektorientiert aufgebaut. Es ähnelt in weiten Zügen der Programmiersprache C++. Multithreaded siehe Kapitel 2.2 Verteilt Die Standardbibliothek von Java bietet bereits Klassen, mit denen verteilte Anwendungen auf Basis von TCP/IP einfach realisiert werden können. Robust Es gibt mehrere Eigenschaften von Java, die die Robustheit der Anwendungen erhöht. Hierfür wurden auf einige Elemente verzichtet, die bei der Programmiersprache C++ oft zu Fehler bzw. Instabilität der Programme führen. • Java verzichtet auf die Arbeit mit Pointern. • Das System kümmert sich selbständig um die Freigabe von Speicherplatz. • Java ist streng Typenprüfung. Sicher objektorientiert mit gleichzeitiger strenger Java wurde ursprünglich für Online-Systeme im Internet entwickelt. Aus diesem Grund wurde besonderen Wert auf die Sicherheit gelegt. Diese Sicherheit ist besonders wichtig, wenn Applets von einem beliebigen Server auf den eigenen Rechner geladen und ausgeführt werden. Da Java-Programme keine echten Binärprogramme für ein bestimmtes Betriebssystem sind, sondern Bytecode für eine virtuelle Maschine, kann in der Umsetzung des Interpreters für die virtuelle Maschine einiges an Sicherheit erreicht werden. 11 2 Programmiersprache Java 2.2 Prozesse / Threads in Java Java ist eine Programmiersprache, die Multithreading unterstützt. Dies bedeutet, daß ein Prozess (Anwendung) in verschiedene Teile aufgeteilt werden kann. Diese Teilaufgaben können so von verschiedenen Threads "parallel" abgearbeitet werden. Inhaltlich sind einzelne Threads eine in sich sequentielle Abfolge von Anweisungen. Als Ganzes gesehen laufen sie jedoch "parallel" zu andern Threads. Das Programmieren von Threads ist z.B. sinnvoll bei der Arbeit mit Applets, da es so möglich ist mehrere Aufgaben unabhängig voneinander ablaufen zu lassen. Ein Thread kann sich z.B. um die Aktualisierung der Bildschirmausgabe kümmern während ein anderer Thread die Kommunikation mit dem Server übernimmt. Zu diesem Zweck bietet Java Primitiven an um Threads zu steuern und zu synchronisieren. 2.3 Zustände von Threads unbekannt new schlafend interrupt() erzeugt suspend() sleep() start() resume() suspend() rechenwillig resume() suspendiert suspend() notify(), interrupt() Scheduler yield() ausgeführt run() zu Ende, stop() resume() blockiert wait(), join() beendet Seit JDK 1.2 verworfen Abb 2.1: Zustände eines Threads [MiSi1999] 12 2 Programmiersprache Java Java Threads können sich wie Threads bzw. Prozesse vom Betriebssystem in verschiedene Zustände befinden. erzeugt: In diesen Zustand gelangen Threads nachdem sie durch den newOperator erzeugt wurden. In diesem Zustand kann man die Priorität einstellen, das Dämon-Flag setzen oder den Thread einer ThreadGruppe zuordnen. rechenwillig: In diesen Zustand gelangt der Thread, wenn er durch den Aufruf der Methode start() gestartet wird. ausgeführt: Der Thread erhält durch den Scheduler der Virtual Machine die CPU und die Ressourcen, die er benötigt um seine run() Methode auszuführen. Mit der Methode yield() kann der Thread die CPU freiwillig wieder freigeben. beendet: In diesen Zustand gelangt ein Thread, wenn er seine run() Methode beendet. Die Methode stop() wurde in der Version 1.2 verworfen, da der Thread keine Möglichkeit hat eventuelle Synchronisationsmechanismen zurückzusetzen. schlafend: Mit der Methode sleep() kann ein Thread seine Ausführung für eine bestimmte Zeit aussetzen. In dieser Zeit kann ein anderer Thread durch den Aufruf von interrupt() veranlassen, daß der "schlafende" Thread wieder in den Zustand rechenwillig wechselt. Ein interrrupt() Aufruf wird dem Thread durch eine InterruptedException signalisiert, damit er zwischen der "normalen" Beendigung der sleep() Methode und einem interrupt() Aufruf unterscheiden kann. Ein "schlafender" Thread behält alle Monitore, die er vor dem sleep() Aufruf besaß. blockiert: In diesen Zustand gelangt ein Thread, wenn er auf eine Ressource wartet oder sich mit einem anderen Thread synchronisiert. Diese Synchronisation kann auf mehrere Arten erfolgen (siehe Synchronisation von Threads). suspendiert: In der Version 1.2 wurden die Aufrufe suspend() und resume() die bewirken, daß ein Thread in den Zustand suspendiert wechselt verworfen, da sie stark Deadlock gefährdet sind. Ein Thread besitzt beim Aufruf dieser beiden Methoden keine Möglichkeit eventuelle Synchronisationsmechanismen zurückzusetzen. 13 2 Programmiersprache Java 2.4 Priorität und Scheduling In Java wird die Priorität von Threads in 10 Stufen eingeteilt. Die niedrigste Priorität ist 1 und die höchste Priorität ist 10. Wird ein Thread erzeugt besitzt er die Priorität 5 (Thread.NORM_PRIORITY). Diese Priorität kann mit der Methode setPriority() geändert werden. Die Methode getPriority() liefert die aktuelle Priorität des Thread. Das Scheduling erfolgt in Java hierarchisch was bedeutet, daß ein Thread erst dann ausgeführt wird, wenn alle Threads mit einer höheren Priorität abgearbeitet sind oder sich nicht im Zustand rechenwillig befinden. Besitzen zwei Threads die selbe Priorität werden keine Angaben über das Scheduling gemacht. In diesem Fall besitzt das Scheduling des Betriebssystem Einfluß auf die Abarbeitung der einzelnen Threads. Es ist daher sinnvoll eine Applikation, die Multithreading nutzt und auf verschiedenen Plattformen laufen soll, auf den einzelnen Plattformen zu testen. 2.5 Dämon-Threads Ein Dämon-Thread ist ein Thread der entweder vom System oder vom Entwickler gestartet wird um im Hintergrund bestimmte Aufgaben auszuführen. Möchte man ein Dämon-Thread erzeugen, so muß man lediglich nach der Erzeugung eines Threads aber vor seinem Start mit dem Aufruf setDaemon(true) das Dämon-Flag des Threads auf true setzen. Mit dem Aufruf isDaemon() kann festgestellt werden ob ein Thread ein Dämon Thread ist oder nicht. Ein Java-Programm wird dann beendet, wenn alle seine "normalen" Threads (auch UserThreads genannt) beendet sind. Dämon-Threads werden hierbei nicht berücksichtigt. Sobald alle User-Threads beendet sind werden auch die Dämon-Threads beendet. Bei der Konzeption eigener Dämon-Threads ist zu berücksichtigen, daß das Programmende für Dämon-Threads asynchron, z.B. mitten in der run() Methode eintreten kann. 14 2 Programmiersprache Java 2.6 Thread Gruppen Threads werden in Java einer Thread Gruppe zugeordnet. In einer sogenannten ThreadGroup können Threads zusammengefasst werden, die von ihrer Zugehörigkeit oder ihrer Aufgabe nach eine Einheit bilden. Wird ein Thread nicht explizit einer Gruppe zugeordnet so wird er der Gruppe zugeordnet, welcher der erzeugende Thread angehört. Ohne besondere Maßnahmen ist dies eine voreingestellte Gruppe, die automatisch von der Virtual Maschine angelegt wird. Bei Applikationen heißt diese Gruppe "main", bei Applets legt der Browser für jedes Applet eine eigene Gruppe an, die den Name der Applet-Klasse trägt. Eine ThreadGroup kann sowohl Threads als auch eine weitere ThreadGroup enthalten. „main“ Thread 1 Thread A1 ThreadGroup „A“ Thread A2 Thread 2 ThreadGroup „B“ Thread B1 Thread B2 Abb. 2.2: Hierarchische Struktur der Thread-Groups [MiSi1999] Wie in Abb. 2.2 zu sehen ist entsteht durch diese Anordnung der Thread Gruppen eine baumartige Struktur. Diese hierarchische Struktur kann in der Klasse SecurityManager verwendet werden um Zugriffsrechte zuzuteilen. Damit soll verhindert werden das ein Threads die Priorität der anderen Threads soweit herabstuft das nur noch er die CPU zugeteilt bekommt. Diese Sicherung gilt allerdings nur für Applets und nicht für "normale" Anwendungen. Der Browser legt für jedes Applet eine eigene Thread Gruppe an, die unter der Hauptgruppe steht, in der die System-Threads laufen. Der SecurityManager erlaubt einem Thread nur Threads seiner Gruppe sowie auf Threads aller Untergruppen zuzugreifen. Durch diese Zugriffssteuerung wird verhindert, daß ein Applet den Ablauf von System-Threads beeinträchtigt oder Threads von anderen Applets manipulieren kann. Folgende Methoden sind von dieser Zugriffssteuerung betroffen: • setPriority() • setName() • setDaemon() • stop(), suspend(), resume() 15 2 Programmiersprache Java 2.7 Erzeugung und Steuerung von Threads Vom objektorientierten Standpunkt ist ein Thread in Java auch ein Objekt. Es gibt in Java zwei verschiedene Möglichkeiten den Code einer Klasse als Thread ausführen zu lassen. 1. Klasse von Thread ableiten Man leitet eine Klasse von der Klasse Thread ab und überschreibt die run() Methode. Um einen Thread zu erzeugen wird der Konstruktor der abgeleiteten Klasse aufgerufen. Gestartet wird dieser Thread anschließend mit dem Aufruf von start(). Beispiel: class MeinThread extends Thread { // abgeleitete Klasse MeinThread (){} ; // Konstruktor public void run { ** Hier steht der Programmcode der als Thread ausgeführt werden soll** } } class EinThread { // Main-Methode MeinThread thread1= new MeinThread(); // thread1 wird erzeugt thread1.start(); // thread1 wird gestartet } 2. Das Interface Runnable Ist eine Klasse bereits von einer andern abgeleitet implementiert man das Interface Runnable damit der Programmcode, der in der run() Methode steht, als Thread ausgeführt wird. Das Erzeugen eines Threads erfolgt hierbei in zwei Stufen. Als erstes wird ein Objekt der Klasse erzeugt. Danach wird dieses Objekt dem Konstruktor Thread(Runnable) übergeben. Beispiel: // Klasse mit Interface Runnable class MeinThread implements Runnable { MeinThread() {} ; // Konstruktor public void run { ** Programmcode der als Thread ausgeführt werden soll** } } class EinThread { // Main-Methode Runnable rthread = new MeinThread(); // Runnable Objekt und Thread thread1 = new Thread(rthread); // thread1 wird erzeugt thread1.start(); // thread1 wird gestartet } 16 2 Programmiersprache Java Der Zusammenhang zwischen Runnable-Objekt und Thread-Objekt ist ähnlich wie zwischen einem Auto und seinem Lenker. • Das Runnable-Objekt entspricht dem Auto. • Das Thread-Objekt entspricht dem Lenker, der das Auto steuert. • Jedes Auto muß von einer Person gelenkt werden, und jede Person kann nur ein Auto lenken. • Der Konstruktor des Thread-Objektes entspricht der Übergabe des Autoschlüssels an den Lenker: Damit wird festgelegt welches Auto er lenken soll. • Die start() Methode entspricht dem Starten mit dem Schlüssel, die run() Methode entspricht dem Laufen des Motors: Der Lenker dreht den Startschlüssel, und bewirkt, daß der Motor zu laufen beginnt. Um einen Thread auf einfache Art zu beenden konnte man bis zur Version 1.1 einfach die Methode Thread.stop() aufrufen. In der Version 1.2 wurde diese Methode allerdings verworfen, da sie unter bestimmten Umständen problematisch ist. Die Methode Thread.stop() beendet den Thread ohne ihm die Möglichkeit zu geben Monitore oder Ergebnisse, die er besitzt, zurückzugeben. Bei den Beispielprogrammen wurde die Methode Thread.stop() angewendet wenn die gesamte Anwendung, mit allen Threads, beendet werden soll, da es in dieser Situation keinen Unterschied macht ob ein Thread eventuell einen Monitor besitzt oder nicht. Besteht der Rumpf eines Threads aus einer Schleife und möchte man diesen Thread so beenden, daß er die Möglichkeit besitzt eventuelle Monitore oder Ergebnisse zurückzugeben sollte statt dessen folgendes Muster anwendet werden: • Beim Start eine Referenz auf den Thread ermitteln. • Zum Anhalten wird diese Referenz auf null gesetzt. • In der Hauptschleife des Threads wird vor jedem Durchgang überprüft, ob diese Referenz noch auf den Thread selbst verweist. Wenn die Referenz den Wert null hat oder bereits auf einen anderen Thread verweist, der in der Zwischenzeit neu erzeugt wurde, beendet der Thread seine Ausführung durch das Verlassen der Schleife. Beispiel : class MeinThread extends Thread { MeinThread () {}; Thread t ; // wird automatisch mit null initialisiert public void start() { if (t == null ) { t = new Thread(this) ; t.start() ; } // Wenn kein Thread angelegt ist, // anlegen und starten des Threads 17 2 Programmiersprache Java public void stop() { t = null ; } // setzen der Referenz auf null public void run() { Thread me = Thread.currentThread(); // Referenz des aktuellen // Threads zuordnen while ( me == t ) { // Referenzen vergleichen ** Programmcode der ausgeführt werden soll ** } } } Java bietet für die Arbeit mit Threads eine große Anzahl von Konstruktoren und Methoden. Konstruktoren und Methoden, die in dieser Diplomarbeit verwendet bzw. angesprochen werden, werden hier eingehender erläutert [MiSi1999]. Das Paket java.lang.Thread Datenelemente: public final static int MIN_PRIORITY = 1 Die niedrigste Priorität, die ein Thread haben kann. public final static int NORM_PRIORITY = 5 Die zunächst für alle Threads voreingestellte Priorität. public final static int MAX_PRIORITY = 10 Die höchste Priorität, die ein Thread haben kann. Konstruktoren: public Thread() Erzeugt einen neuen Thread. public Thread(Runnable target) Erzeugt einen neuen Thread für die Runnable-Implementierung target. Methoden: currentThread public static native Thread currentThread() Liefert einen Verweis auf den momentan aktiven Thread. getName public final String getName() Liefert den Namen des Threads. getPriority public final int getPriority() Liefert die Priorität des Threads. 18 2 Programmiersprache Java isAlive public final native boolean isAlive() Liefert true, wenn der Thread lauffähig ist. Dies ist der Fall, wenn seine start() Methode, aber noch nicht seine stop() Methode aufgerufen wurde. Der Rückgabewert false signalisiert, daß der Thread nicht lauffähig ist. Das ist der Fall, wenn der Thread neu erzeugt wurde, ohne daß bisher seine start() Methode aufgerufen wurde, oder wenn die Ausführung seiner run() Methode ordnungsgemäß beendet oder seine stop() Methode aufgerufen wurde. isDaemon public final boolean isDaemon() Liefert true, wenn der Thread als Daemon läuft, sonst false. join public final void join() Wartet auf die Beendigung des Threads. run public void run() Diese Methode enthält den eigentlichen Anweisungsteil, der nach der Aktivierung ausgeführt wird. Hierzu muß sie überschrieben werden, wenn beim Aufruf des Konstruktors kein Runnable-Objekt übergeben wurde. Falls dem Konstruktor ein Runnable-Objekt übergeben wurde, wird dessen run() Methode ausgeführt. setDaemon public final void setDaemon(boolean on) Läßt den Thread als Daemon oder User-Thread ausführen, wenn on true bzw. false ist. setPriority public final void setPriority(int newPriority) Setzt die Priorität des Threads auf newPriority. newPriority muß im Bereich von MIN_PRIORITY und MAX_PRIORITY liegen. sleep public static native void sleep(long millis) Bewirkt, daß die Ausführung des Threads millis Millisekunden ausgesetzt wird. start public synchronized native void start() Startet den Thread durch Aufruf der run-Methode. stop public final void stop() Hält den Thread an. Diese Methode wurde in Version 1.2 verworfen, da sie Inkonsistenzen verursachen kann, wenn sie bei einem Thread aufgerufen wird, der gerade eine Zugangsmethode eines Monitors ausführt. yield public static native void yield() Mit dem Aufruf dieser Methode unterbricht der Thread seine Ausführung, um anderen Threads mit der gleichen Priorität die Möglichkeit zur Ausführung zu geben. yield() bewirkt nichts, wenn momentan keine Threads verfügbar sind, die dieselbe Priorität haben und rechenwillig sind. 19 2 Programmiersprache Java 2.8 Synchronisation In Java gibt es, wie in Unix oder Windows NT, die Möglichkeit, Threads zu synchronisieren, damit sie, obwohl sie normalerweise unabhängig von einander ausgeführt werden, aufeinander abgestimmt werden können. 2.8.1 Synchronisation durch Priorität Durch die Priorität eines Threads wird in Java die Reihenfolge festgelegt, in der die Threads einer Applikation oder eines Applets abgearbeitet werden (siehe Kapitel 2.4). Eine Synchronisation zwischen den Prozessen findet aber nicht statt. Deshalb darf man streng genommen hier nicht von einer Synchronisation sprechen sondern nur von der Bevorzugung eines bestimmten Threads. Um die Priorität eines Threads zu verändern verwendet man die Methode setPriority(). Die Priorität kann von 1 - 10 eingestellt werden. Es kann zu Schwierigkeiten kommt, wenn man die Priorität eines rechenintensieven Threads zu hoch setzt (z.B. auf 10). In so einem Fall werden die System-Threads eventuell vernachlässigt, so daß das System nicht mehr einwandfrei läuft. Bei einem Applet kann die Priorität nicht beliebig erhöht werden. Das System verhindert, das ein Applet durch seine Threads das System blockiert. Mit der Methode getPriority() erhält man die aktuelle Priorität des Threads. 2.8.2 Synchronisation durch Monitore Das Konzept des Monitors paßt von der Konstruktion her sehr gut zur objektorientierten Programmierung. Unter einem Monitor kann man sich ein Objekt vorstellen, das bestimmte Attribute in sich einschließt, auf die nur über definierte Zugangsmethoden zugegriffen werden kann. Der Monitor ist besetzt, sobald eine Zugangsmethode ausgeführt wird. Zu jeder Zugangsmethode verwaltet der Monitor eine Warteschlange, in die Threads eingereiht werden, die eine solche Methode aufrufen, während der Monitor besetzt ist. Durch das Einfügen des Modifier synchronized in der Deklaration der Methode erhält man so einen Monitor. Sobald eine mit syncronized deklarierte Methode von einem Thread ausgeführt wird, kann kein anderer Thread eine synchronized Zugangsmethode von diesem Objekt mehr ausführen. Das folgende Beispiel soll schemenhaft zeigen, wie ein solcher Monitor angewendet werden kann. Zwei Threads rufen hierbei mit unterschiedlicher Geschwindigkeit die Methode put() der Klasse Ausgabe auf. Obwohl diese Methode absichtlich zeitaufwendig gestaltet wurde wird sie zu Ende geführt obwohl ein anderer Thread sie zwischenzeitlich ebenfalls aufgerufen hat. (Das ausführliche Beispiel mit der Bildschirmausgabe befindet sich im 6. Kapitel dieser Diplomarbeit). 20 2 Programmiersprache Java Beispiel für die Synchronisierung einer Methode: class Ausgabe { ... // Übergabe eines Strings und der Nummer des Threads public synchronized void put (String ausgabe, int nummer) { ... System.out.println ("Thread Nummmer"+nummer+"beginnt mit der Methode"); System.out.println (ausgabe); // Ausgabe des Strings System.out.println ("Thread Nummmer"+nummer+"beendet die Methode"); } } 2.8.3 Synchronisation durch Sperr-Objekte Benötigen die Threads mehrere Ressourcen gleichzeitig, so kann es schnell zu einem Deadlock kommen wenn synchronized auf Methoden angewendet wird. Bei einem Java Programm kann dies geschehen, wenn sich zwei Monitore in den Zugangsmethoden gegenseitig aufrufen. In diesem Fall kann es vorkommen, daß ein Thread den Monitor A betritt und ein anderer den Monitor B. Wenn nun in der Zugangsmethode von A eine Zugangsmethode von B aufgerufen wird und in B eine von A, so wartet der erste Thread darauf, daß der zweite den Monitor B verläßt, dieser wartet jedoch darauf, daß der erste den Monitor A verläßt. Benötigt ein oder mehrere Threads mehrere Ressourcen gleichzeitig kann der Zugriff auf diese Ressourcen durch das Schlüsselwort synchronized auf eine zweite Art verwendet werden. Beispiel für die Synchronisation mit einem Sperr-Objekt: class SteuerObj {} class MeineKlasse { SteuerObj Objekt1 = new SteuerObj() ; .... public void Methode() { synchronized(Objekt1) { // kritischer Bereich ** Programmcode der synchronisiert werden soll ** } } Wird synchronized wie im Beispiel gezeigt eingesetzt, erfolgt die Synchronisierung anhand eines Sperr-Objektes. In diesem Beispiel ist das Sperr-Objekt "Objekt1". 21 2 Programmiersprache Java Bei dieser Art der Synchronisation kann ein Teil einer Methode nicht synchronisiert und ein anderer Teil synchronisiert ausgeführt werden. Dies hat den Vorteil, daß der synchronisierte Teil so kurz wie möglich gehalten werden kann, egal wie lang die eigentliche Methode ist. Ein weiterer Vorteil besteht darin, daß wenn es nötig ist diese synchronized Blöcke geschachtelt werden können. Beispiel für geschachtelte Sperr-Objekte: synchronized(Objekt1) { synchronized(Objekt2) { synchronized(Objekt3) { // kritischer Bereich ** Programmcode der synchronisiert werden soll ** } } } Bei einer geschachtelten Synchronisation muß allerdings streng darauf geachtet werden, daß die Threads diese Schachtelung in der gleichen Reihenfolge durchlaufen, da sonst ein Deadlock entsteht. Beispiel wie ein Deadlock entstehen kann: // Thread 1 synchronized(Objekt1) { synchronized(Objekt2) { ..... } } // Thread 2 synchronized(Objekt2) { synchronized(Objekt1) { ..... } } Wenn bei diesem Beispiel Thread 1 das Objekt1 belegt und Thread 2 direkt danach Objekt2 in seinen Besitz nimmt entsteht ein Deadlock, da beide Threads jeweils das Sperr-Objekt des anderen Threads benötigen um ihren Programmcode weiter auszuführen. 22 2 Programmiersprache Java 2.8.4 Synchronisation durch Klassenbezogene-Sperre Wenn man sichergehen muß, daß ein kritischer Bereich für sämtliche Threads synchronisiert ist muß dieses Synchronisations Objekt das definitiv einzige Exemplar seiner Klasse sein. In so einem Fall kann man das Class-Objekt verwenden. Das Class-Objekt einer Klasse ist einmalig in einer Virtual Machine (insofern ist es auch gerechtfertigt, von »dem« Class-Objekt zu sprechen). Daher können mit diesem Objekt alle Threads einer Virtual Machine synchronisiert werden, auch dann, wenn sie unterschiedliche Exemplare der Monitor-Klassen halten oder wenn von der MonitorKlasse gar keine Exemplare erzeugt werden können, weil sie ausschließlich statische Zugangsmethoden definieren. Diese Art der Synchronisation kann z.B. bei der Arbeit mit Applets eingesetzt werden. Obwohl sie völlig getrennte Programme sind laufen sie in der selben Virtual Machine im Browser. Es ist dabei unwichtig ob sich die Applets in verschiedenen Dokumenten vom gleichen Host befinden und in getrennten Fenster angezeigt werden. In dem folgendem Beispiel synchronisieren sich zwei Applets mit Hilfe der Klasse StaticMonitor indem die Methoden put() und get() jeweils von einem Applet aufgerufen werden: public class StaticMonitor { static int buffer; static boolean empty = true; public synchronized static void put(int data) { try { Class classLock = StaticMonitor.class; // Class-Objekt wird // ermittelt if (!empty) // Wenn der Speicher voll ist classLock.wait(); // wird gewartet. buffer = data; empty = false; // Speicher wird auf leer gesetzt classLock.notify(); // Ein eventuell wartender Thread } // wird benachrichtigt und kann jetzt catch(InterruptedException e) { } // weiter arbeiten } 23 2 Programmiersprache Java public synchronized static int get() { try { Class classLock = StaticMonitor.class; // Class-Objekt // wird ermittelt if (empty) // Wenn der Speicher leer ist classLock.wait(); // wird gewartet. System.out.println( "Abgeholt: "+ buffer ); empty = true; // Speicher wird auf voll gesetzt. classLock.notify(); // Ein eventuell wartender Thread } // wird benachrichtigt und kann jetzt catch(InterruptedException e) {} // weiter arbeiten. return buffer; } } Wie in diesem Beispiel zu sehen ist wird zuerst das Class-Objekt in beiden Methoden ermittelt. Die Synchronisation findet dann über dieses Objekt mittels wait() und notify() statt. Dadurch, daß die beiden Zugangsmethoden als static vereinbart sind wird das klassenbezogene Sperren erreicht. Vom grundsätzlichem Aufbau her stimmen die Zugangsmethoden mit denen, die bei der Synchronisation mit Ereignissen verwendet werden, überein. Methoden für das klassenbezogene Sperren und der Synchronisation mit Ereignissen: wait public final void wait() Wartet auf den Eintritt eines Ereignisses. Diese Methode kann nur aus synchronisierten Methoden heraus aufgerufen werden. notify public final native void notify() Benachrichtigt einen wartenden Thread von einem Ereignis. Kann nur in synchronisierten Methoden aufgerufen werden. 24 2 Programmiersprache Java 2.8.5 Synchronisation durch Ereignisse Bei der Synchronisation mit Ereignissen besitzt der Programmierer eine größere Kontrolle über die Abläufe im Zusammenhang mit Monitoren. So kann der Programmierer z.B. den Wert eines Datenelementes vorgeben, der vorhanden sein muß damit ein Thread den Monitor erhält. Ist dies nicht der Fall muß der Thread warten bis das der richtige Wert bzw. das Ereignis eintritt. In dem Beispielprogramm zu dieser Synchronisationsart wurde die Anzahl der Bildschirmausgaben, die jeder Thread ohne Unterbrechung ausführt, als Ereignis verwendet (vollständiges Programm und Bildschirmausgabe ist in Kapitel 6 abgedruckt). Beispiel für die Synchronisation mit Ereignissen: class Ausgabe { private int anzahl=0; private int soll=3; boolean th1 = true; // Anzahl der Ausgaben // Variable die angibt, welcher // Thread den Monitor erhält. // Synchronisierte Methode, die von Thread 1 aufgerufen wird. public synchronized void put1 (String ausgabe) { if (!th1) // Wenn th1 == false try {wait();} // muß Thread 1 warten. catch(InterruptedException e) {} this.buffer=ausgabe; System.out.println(buffer); // Hier erfolgt die Ausgabe auf // dem Bildschirm. anzahl=anzahl+1; // Anzahl der Ausgaben wird heraufgesetzt. if (anzahl>=soll) // Anzahl der Ausgaben wird mit dem { // Sollwert verglichen. anzahl=0; // Anzahl der Ausgaben wird auf 0 gesetzt. th1=false; // Variable für den Monitor wird umgestellt notify(); // Eventuell wartender Thread erhält die } // Benachrichtigung, daß er } // jetzt weiter arbeiten kann. } 25 2 Programmiersprache Java // Synchronisierte Methode, die von Thread 2 aufgerufen wird. public synchronized void put2 (String ausgabe) { if (th1) // Wenn th1 == true try {wait();} // muß Thread 2 warten. catch(InterruptedException e) {} this.buffer=ausgabe; System.out.println(buffer); // Hier erfolgt die Ausgabe // auf dem Bildschirm. anzahl=anzahl+1; // Anzahl der Ausgaben wird heraufgesetzt if (anzahl>=soll) // Anzahl der Ausgaben wird mit dem // Sollwert verglichen. { anzahl=0; // Anzahl der Ausgaben wird auf 0 gesetzt. th1=true; // Variable für den Monitor wird umgestellt notify(); // Eventuell wartender Thread erhält die } // Benachrichtigung, daß er jetzt } // weiter arbeiten kann. } 2.8.6 Synchronisation auf das Ende von anderen Threads In Java definiert die Klasse Thread die Methode join(). Diese Methode kann verwendet werden um einen Thread auf das Ende eines anderen Threads zu synchronisieren. Mit join() können auf einfache Weise Verarbeitungsketten implementiert werden, bei denen ein Thread auf die Fertigstellung eines Zwischenergebnisses warten muß, welches wiederum von anderen Threads berechnet wird. Mit dem Aufruf thread2.join() wartet der aufrufende Thread bis der Thread "thread2" seine Ausführung beendet hat. Es gibt bei der Verwendung von join() einige Details, die beachtet werden müssen: • Die Methode join() kann mehrmals hintereinander aufgerufen werden und bewirkt so, daß auf mehrere Threads gewartet wird. Die Reihenfolge ist hierbei beliebig. Der aufrufende Thread wartet solange bis der letzte Thread auf den gewartet werden soll beendet ist. • Bei der Implementierung ist darauf zu achten, daß alle Threads auf die gewartet werden soll bereits gestartet sind bevor die Methode join() von einem anderen Thread verwendet wird. Der Thread, der auf das Ende von anderen Threads wartet muß als letztes gestartet werden. Bei der Implementierung von mehreren Threads die wiederum auf das Ende von mehreren Threads warten kann es daher kompliziert werden die richtige Reihenfolge beim Starten der Threads einzuhalten. • Erfolgt z.B. der Aufruf thread2.join() und der Thread "thread2" ist bereits beendet, so bleibt diese Methode ohne Wirkung und der aufrufende Thread wird fortgesetzt. 26 2 Programmiersprache Java Beispiel für die Synchronisation auf das Ende eines anderen Threads: class CountingThread extends Thread { // Modifizierte start()-Methode. Es wird der // Thread übergeben, auf den gewartet werden soll. public void start(Thread thread) { try{thread.join();} catch(InterruptedException e) {} super.start(); } // Ist der Thread, auf den gewartet wird zu // Ende wird der wartende Thread gestartet. public void run(){ ** Programmcode, der als Thread ausgeführt werden soll ** } } class MeinThread2 extends Thread { // "Normaler" Thread, auf den // gewartet werden soll. public void run() { ** Programmcode, der als Thread ausgeführt werden soll ** } } class AufEndeThread { // Main-Methode ... CountingThread thread1 = new CountingThread(); // Erzeugen der MeinThread2 thread2 = new MeinThread2(); // beiden Threads thread2.start(); thread1.start(thread2); } // // // // // // Thread, auf den gewartet werden soll wird zuerst gestartet. Thread, der warten soll wird gestartet. Ihm wird der Thread "thread2" übergeben auf den er warten soll. 27 2 Programmiersprache Java 2.9 Kommunikation zwischen Threads mit Piped-Streams Die Ein- und Ausgabe wird in Java mit Hilfe sogenannter Streams realisiert. Einen Stream kann man sich als eine Art Pipeline vorstellen (siehe Abb. 2.3). Am "Schreibende" wird der Stream mit Daten gefüllt und am "Leseende" werden diese Daten in der selben Reihenfolge wieder herausgeholt. Es können dabei keine Daten verloren gehen, da sie bis zum Auslesen in der Pipe verweilen. Streams sind in Java immer unidirektional. Daten Schreiben Lesen Stream Abb. 2.3: Die Arbeitsweise von Streams [MiSi1999] Das Paket java.io stellt dem Programmierer verschiedene Klassen zur Verfügung, die auf dem Streamkonzept basieren. In dieser Diplomarbeit, in der es um die Verwendung von Threads bzw. Prozessen geht, werden die Piped-Streams, die für einen Datenaustausch zwischen Threads geeignet sind, eingehender besprochen. Pipes werden auch in Windows NT und Unix eingesetzt. Die genaue Arbeitsweise ist dabei vom jeweiligem System abhänig und wird daher separat beschrieben. Eine Pipe kann in Java mit Hilfe der Klassen PipedReader und PipedWriter realisiert werden und so die Kommunikation zwischen zwei Threads ermöglichen. Mit einer Pipe könne Streams direkt miteinander gekoppelt werden. Diese Kopplung der Streams kann auf verschiedene Arten erfolgen: • Direkt bei der Erzeugung eines PipedReader oder PipedWriter Objektes: • PipedReader p = new PipedReader (myPipedWriter); • PipedWriter p = new PipedWriter (myPipedReader); • Die Streams können aber auch ohne Parameter initialisiert werden und später durch die Methode connect() verbunden werden. Diese Methode ist in beiden Klassen PipedReader und PipedWriter enthalten. 28 2 Programmiersprache Java Beispiel für die Kommunikation von zwei Threads über eine Pipe: class CountingThread extends Thread { PipedInputStream pipein; // Konstruktor des Threads public CountingThread(PipedInputStream pipein) { this.pipein = pipein; } // Stream Objekt zum Lesen // der Pipe wird übernommen. public void run() { while(true) { try {ende=pipein.read(lese,0,1);} catch (IOException e) {} // Pipe wird ausgelesen if(ende == -1) break; // bis -1 zurückgegeben wird. ... } try {pipein.close();} catch(IOException e){} // Schließt den Stream } } class CountingThreadRunnable implements Runnable { PipedOutputStream pipeout; // Konstruktor public CountingThreadRunnable(PipedOutputStream pipeout){ this.pipeout = pipeout; } // Stream Objekt zum Schreiben // in die Pipe wird übernommen. public void run(){ ... for(hilf=0;hilf<=10;hilf++) // 10 mal wird eine Zahl { // in die Pipe geschrieben try {pipeout.write(schreibe,0,1);} catch(IOException e){} } try {pipeout.close();} // Schließt den Stream catch(IOException e){} } } 29 2 Programmiersprache Java class Pipe { // Main-Methode public static void main(String[] args) { // Erzeugen der Stream- Objekte zum Schreiben und Lesen. PipedInputStream pipein = new PipedInputStream(); PipedOutputStream pipeout = new PipedOutputStream(); try {pipeout.connect(pipein);} catch (IOException e) {} // Kopplung der Streams // Erzeugung der Threads mit Übergabe der Stream-Objekte. Runnable rthread = new CountingThreadRunnable(pipeout); Thread thread2 = new Thread(rthread); CountingThread thread1 = new CountingThread(pipein); thread1.start(); thread2.start(); } } // Starten der Threads Die Methoden bzw. Konstruktoren der beiden Klassen PipedReader und PipedWriter werden hier genauer erläutert [MiSi1999]: Die Klasse PipedReader: Konstruktoren: public PipedReader() Erzeugt einen neuen PipedReader, der noch keine Verbindung besitzt. Vor der Benutzung muß er noch mit einem PipedWriter verbunden werden (entweder mit connect() oder dem Konstruktor von PipedWriter ). public PipedReader(PipedWriter src) Erzeugt einen neuen PipedReader, der mit dem PipedWriter src verbunden ist. Methoden: close public void close() Schließt den Stream. connect public void connect(PipedWriter src) Verbindet den Stream mit src. 30 2 Programmiersprache Java read public int read(char[] b, int off, int len) Versucht, len Zeichen aus dem Stream zu lesen und speichert sie ab dem Index off in b. Wenn beim Versuch, das erste Zeichen zu lesen, das Dateiende bereits erreicht ist, ist der Rückgabewert -1. Ansonsten wird die Anzahl der tatsächlich gelesenen Bytes zurückgeliefert. Wenn während des Lesevorgangs das Stream-Ende erreicht wird oder wenn im darunterliegendem Stream nur weniger als len Bytes gelesen werden können, ohne zu blockieren, kehrt die Methode zurück und liefert die Anzahl der bis dahin gelesenen Bytes. Die Klasse PipedWriter: Konstruktoren: public PipedWriter() Erzeugt einen neuen PipedWriter, der noch keine Verbindung besitzt. Vor der Benutzung muß er noch mit einem PipedReader verbunden werden (entweder mit connect() oder dem Konstruktor von PipedReader ). public PipedWriter(PipedReader sink) Erzeugt einen neuen PipedWriter, der mit dem PipedReader sink verbunden ist. Methoden: close public void close() Schließt den Stream. Falls der Stream bereits geschlossen war, bleibt die Methode ohne Auswirkungen. connect public void connect(PipedReader sink) Verbindet den Stream mit sink. flush public void flush() Bewirkt, daß noch gepufferte Daten in den Stream geschrieben werden. write public void write(char[] b, int off, int len) Schreibt die ersten len-Zeichen ab dem Index off aus dem Array b in den Stream. 31 3 Betriebssystem Windows NT 4.0 3 Betriebssytem Windows NT 4.0 3.1 Grundlegende Eigenschaften Windows NT ist seit 1993 in Deutschland erhältlich. Die Version 4.0 wurde 1996 herausgegeben. Der Nachfolger Windows 2000 ist seit Ende 1999 bzw. Anfang 2000 erhältlich. Windows NT ist ein 32-Bit Betriebssystem mit Multiprocessing und Multithreadingfähigkeit. Windows NT setzt nicht auf DOS auf, sondern ist ein absolut eigenständiges Betriebsystem. Programme werden unter Windows NT in getrennten Speicherbereichen ausgeführt, so daß selbst bei Programmabstürzen das System nicht in Mitleidenschaft gezogen wird. Jeder Benutzer hat seine eigene Arbeitsumgebung, die er selbst gestalten kann und die ihm bei jeder neuen Anmeldung wieder zur Verfügung steht. In einem Windows NT Netzwerk kann diese Umgebung sogar mit auf andere Rechner wandern. Bei Windows NT kann im Gegensatz zu z.B. LINUX nur ein Benutzer interaktiv mit dem System arbeiten und Programme ausführen. Windows NT ist kein Multiuser-System, auf dem gleichzeitig viele Benutzer Programme ablaufen lassen können. Gleichwohl können mehrere Benutzer Windows NT als DateiServer oder Web-Server nutzen. Bei Server-Lösungen im sicherheitsrelevanten Bereichen, die gegen Angriffe aus dem Internet geschützt werden müssen, kann es sehr sinnvoll sein LINUX statt Windows NT oder SOLARIS in Betracht zu ziehen. Hier kann der erfahrene Administrator bei Bekanntwerden eines Fehlers selbst sofort den Code des Betriebssystem ändern. Bei Windows NT ist man auf den Hotfix von Microsoft angewiesen, der natürlich nicht sofort verfügbar ist. 3.2 Prozesse / Threads in Windows NT In Windows NT ist ein Prozess die laufende Instanz einer Anwendung. Er setzt sich aus Codeabschnitten im Speicher zusammen, die aus Programmen und DLL's geladen wurden. Jeder Prozess verfügt über seinen eigenen Adreßraum und seine eigenen Ressourcen, wie z.B. Threads, Dateien und dynamisch reservierten Speicher. Prozesse selbst führen keinen Code aus; sie stellen den Adressraum dar, in denen sich der Code befindet. Der Code im Adreßraum eines Prozesses wird durch einen Thread abgearbeitet, wobei jeder Windows NT Prozess mindestens einen ausführenden Thread besitzt. 32 3 Betriebssystem Windows NT 4.0 Windows NT-Prozesse besitzen verschiedene Merkmale: • Sie sind als Objekte implementiert, und man greift mittels Objektdienste auf sie zu. • Sie können mehrere ausführende Threads in ihrem Adreßraum beherbergen. • Sowohl Prozess- als auch Thread-Objekte besitzen eingebaute Synchronisationsmechanismen. • Zwischen Windows NT und einem seiner Prozesse herrscht keine Vater/SohnBeziehung. Ein Prozess besteht aus mehreren Threads, von denen jeder eine sog. Zeitscheibe vom Windows NT Kernel zyklisch zugewiesen bekommt, während der er abgearbeitet wird. Beendet der letzte Thread seine Aufgabe, so terminiert der Prozess. [HaWi1997] 3.3 Zustände von Prozessen / Threads Prozesse und Threads besitzen feste Momente in ihrem Leben wie Einrichten, Ausführen von Aktionen und Sterben. Nachdem ein Thread eingerichtet wurde, durchläuft er (nach[Hamilton97]) die folgenden Stadien: • Ready: Diese Threads sind einsatz- bzw. ablaufbereit und werden einzig vom Scheduler beachtet. • Standby: Das ist der Thread, der als nächstes "am Zug" ist. Dieser Thread läuft als nächstes, wenn das System nicht die Priorität ändert oder ein Interrupt auftritt. • Running: Dieser Thread wird solange ausgeführt, bis seine Zeitscheibe abgelaufen ist, er freiwillig in den Wartezustand geht, er unterbrochen oder terminiert wird. • Waiting: Normalerweise geht ein Thread in den Wartezustand, wenn er auf I/O-Informationen wartet, die er zum Weiterarbeiten benötigt. Sobald diese Ressource verfügbar ist, geht er in den "Ready"-Zustand über. • Transition: Ein ablaufbereiter Thread, dessen Ressourcen nicht verfügbar sind. • Terminated: Wird ein Thread terminiert, so wird das Objekt des Threads manchmal nicht sofort gelöscht; es kann noch länger verfügbar bleiben, um es zu reinitalisieren und wieder zu verwenden. 33 3 Betriebssystem Windows NT 4.0 3.4 Scheduling In Windows NT wird die Steuerung von Threads durch den sogenannten Dispetcher geregelt. Der Dispetcher verwaltet eine Dispetscher Ready Queue, eine Liste in der alle Threads, die sich im Zustand Ready befinden, ihrer Priorität nach geordnet sind. Jeder Thread erhält einen bestimmten Anteil der CPU-Zeit auch Zeitscheibe genannt. Besitzen mehrer Threads die selbe Priorität so erhalten all diese Threads eine gleich großen Anteil der CPU-Zeit. Threads mit einer geringeren Priorität werden erst berücksichtigt, wenn alle Threads mit einer höheren Priorität abgearbeitet sind. Die Prioritäten in Windows NT sind in zwei Hauptgruppen unterteilt Echtzeit-Prioritäten (Prioritätswert 16-31) und der Bereich der variablen Priorität (Prioritätswert 0-15). Der einzige Thread mit der Priorität 0 ist der "idle" Thread des Kernel-Thread -Objekts, der auf Deferred-Process-Ojekte wartet. Die meisten Threads besitzen eine Priorität zwischen 1 und 15. Um ein "Verhungern" des Systems zu vermeiden , werden die variablen Prioritäten bestimmter Threads entweder nach oben gesetzt, z.B. +2, um die Performence des vordersten Tasks zu steigern, oder auch erniedrigt, falls ein Thread Ressourcen zu horten beginnt. Ein stark CPU-lastiger Prozess / Thread, der zudem eine entsprechend hohe Priorität besitzt wird, während jeder aktiven Phase um eins herabgestuft. Langsam begibt er sich wieder auf das Niveau seiner Basispriorität. Dieses Verfahren der dynamischen Prioritätenanpassung macht Windows NT relativ robust. 34 3 Betriebssystem Windows NT 4.0 3.5 Erzeugung und Steuerung von Prozessen / Threads In Windows NT 4.0 wird ein Prozess gestartet, sobald eine Anwendung aufgerufen wird. Dieser Prozess eignet sich Speicher, Systemressourcen und Threads an, die er zur Ausführung der Anwendung benötigt. Ein Thread ist in Windows NT die kleinste ausführbare Einheit. Beim Starten eines Prozesses wird automatisch mindestens 1 Thread gestartet. Solange noch ein Thread ausgeführt wird, der mit dem Prozess verknüpft ist bleibt dieser bestehen. Ein Prozess hat also mindestens einen Thread. Oft besitzt ein Prozess aber auch mehrere Threads zu unterschiedlichen Zeitpunkten (siehe Abb. 3.1). Ein Thread ist aber immer einem bestimmten Prozess zugeordnet und existiert nur innerhalb dieses Prozesses. Anders als in Unix wird in Windows NT zwischen Prozessen und Threads unterschieden. Ein Windows-NT-Thread ist für das System weniger aufwendig als z.B. ein Prozess in Unix. Obwohl in Windows NT Prozesse in vielen Fällen genau so gehandhabt werden können wie Threads befaßt sich diese Diplomarbeit hauptsächlich mit den Threads, die innerhalb eines Prozesses (Anwendung) erzeugt und gesteuert werden, da sie vergleichbar zu den Threads in Java und den “Sohnprozessen“ in Unix sind. Bei Windows NT wird nicht wie bei Unix eine Kopie des aufrufenden Prozesses erzeugt. Beim Aufruf der CreatThread() Funktion wird ein Thread kreiert, der dann eine bestimmte Funktion ausführt. Time Process GUI Repaginate Read File Print Abb. 3.1: Typischer Windows-NT-Prozess mit seinen Threads [HaWi1997] 35 3 Betriebssystem Windows NT 4.0 Funktionen zum Arbeiten mit Threads unter Windows NT die anhand von beispielhaften C/C++ Code erläutert werden: Zum Erzeugen eines Threads ruft man die Funktion CreateThread() auf. Beispiel CreateThread(): Long WINAPI Print (long Parameter) { ** Hier wird das Programm des Threads ausgeführt ** } unsigned long nThreadID ; HANDLE hThread = CreateThread ( NULL, 0, (LPTHREAD_START_ROUTINE)Print, (void*)1, 0, &nThreadID ); Parameter: • Ein Pointer auf die SECURITY_ATTRIBUTES-Struktur, die die Sicherheitsattribute für den neuen Thread festlegen. Wird hier NULL angegeben weist das Betriebssystem dem Thread einen Standard-Sicherheits-Deskriptor zu. • Stackgröße für den Thread. Bei NULL erhält der Thread eine Standardgröße, die Windows NT bei Bedarf anpasst. • Die Adresse (Name der Funktion) bei der der Thread seine Ausführung beginnt. • Ein 32-Bit-Parameter, der beim Starten der Funktion übergeben werden kann (hier z.B. die frei gewählte Nummer des Threads). • Ist dieses Flag NULL wird der Thread direkt ausgeführt. Wird hier CREATE_SUSPENDED eingesetzt wartet der Thread bis er durch ResumeThread() aufgerufen wird. • Hier wird die Adresse der 32-Bit-Variablen angegeben, in die diese Funktion die Thread-ID speichern soll. Als Rückgabewert dieser Funktion erhält man einen Handle auf den erzeugten Thread. Bei einem Fehler wird NULL zurückgegeben. 36 3 Betriebssystem Windows NT 4.0 Ein Thread Handle sollte nachdem er nicht mehr benötigt wird durch die Funktion CloseHandle() geschlossen werden. Das Betriebssystem schließt einen Handle auch automatisch sobald kein Prozess oder Thread mehr existiert der diesen Handle benötigt. Beispiel CloseHandle(): CloseHandle ( hThread ); Parameter: • Handle der geschlossen werden soll. Um einen Thread zu beenden gibt es mehrere Methoden. Der Thread kann von sich aus die Funktion ExitThread() aufrufen, sich selber beenden und einen Rückgabewert übermitteln. Beispiel: ExitThread(NO_ERROR); Die ExitThread() Funktion wird auch automatisch aufgerufen wenn der Thread seinen Programmcode abgearbeitet hat. Beispiel für einen automatischen Aufruf von ExitThread() : long WINAPI Threadfunktion() { for (int i=0; i<3; i++) { ** In dieser Schleife wird } return NO_ERROR; } // Funktion die der Thread ausführt 3 mal etwas ausgeführt ** // Hier wird der Thread beendet. // Er übergibt NO_ERROR. 37 3 Betriebssystem Windows NT 4.0 Durch die Funktion TerminateThread() kann der Thread beendet werden. Diese Methode ist allerdings nicht empfehlenswert, da der Thread hierbei keine Möglichkeit besitzt seine Ausführung solange fortzusetzen, bis er z.B. einen kritischen Bereich verlassen hat, können Fehler in der Synchronisation enstehen. Beispiel TerminateThread(): TerminateThread ( hThread, NO_ERROR ); Parameter: • Handle für den zu beendenden Thread. • Exit-Code für diesen Thread. Den Exit-Code eines Threads kann man mit der Funktion GetExitCodeThread() erhalten. Wird diese Funktion für einen Thread aufgerufen, der noch nicht beendet ist, liefert diese Funktion STILL_ACTIV zurück. Beispiel GetExitCodeThread(): DWORD dwResult; GetExitCodeThread ( hThread, &dwResult ); If ( dwResult != NO_ERROR ) { ** Dieser Programmteil wird im Falle eines Fehler ausgeführt ** } Parameter: • Handle für den zu befragenden Thread. • Angabe der Adresse einer 32-Bit-Variable, in die der Exit-Code gespeichert werden soll. 38 3 Betriebssystem Windows NT 4.0 Ein weiterer wichtiger Punkt mit der Arbeit von Threads und Prozessen in Windows NT ist die jeweilige Priorität. In Windows NT gilt je höher der Prioritätswert, desto “dringender“ ist der Prozess bzw. Thread. Diese Priorität kann vom Benutzer verändert werden. Das Betriebssystem hat allerdings auch Einfluß auf die Priorität. In Windows NT besitzt der Programmierer aber einen viel stärkeren Einfluß auf die Systemabhängige automatische Steuerung der Prozesse und Threads als in Unix. Es ist daher darauf zu achten, das man die Priorität einzelner Prozesse und Threads nicht zu stark anhebt, da ansonsten das System nicht mehr optimal arbeiten kann. Damit dies nicht geschieht gibt es in Windows NT verschiedene Klassen von Prioritäten für Prozesse (siehe Abb. 3.2). • • • • IDLE_PRIORITY_CLASS NORMAL_PRIORITY_CLASS HIGH_PRIORITY_CLASS REALTIME_PRIORITY_CLASS 31 30 29 28 27 26 25 24 REALTIME_PRIORITY_CLASS 23 22 21 20 19 18 17 16 15 14 13 HIGH_PRIORITY_CLASS 12 11 10 9 NORMAL_PRIORITY_CLASS (Foreground) NORMAL_PRIORITY_CLASS (Background) 8 7 6 5 4 IDLE_PRIORITY_CLASS 3 2 1 Abb. 3.2: Proiritätsstufen in Windows NT [HaWi1997] Die Priorität eines Threads kann mit der Funktion SetThreadPriority() verändert werden. Dies gilt allerdings nur für die relative Priorität des Threads. Die Prioritätsklasse ist abhängig davon welche Klasse der Prozess besitzt, der den Thread gestartet hat. Ein Prozess kann nicht einen Thread in der Prioritätsklasse IDLE_PRIORITY_CLASS besitzen und einen anderen in der REALTIME_PRIORITY_CLASS. 39 3 Betriebssystem Windows NT 4.0 Beispiel SetThreadPriority(): SetThreadPriority ( hThread, THREAD_PRIORITY_LOWEST ); Parameter: • Handle für den Thread, dessen Priorität geändert werden soll. • Prioritätsklasse des Threads innerhalb der Prozess-Prioritätsklasse. • THREAD_PRIORITY_LOWEST 2 Stufen unter der Prioritätsklasse des Prozesses. • THREAD_PRIORITY_BELOW_NORMAL 1 Stufe unter der Prioritätsklasse des Prozesses. • THREAD_PRIORITY_NORMAL Prioritätsklasse des Prozesses • THREAD_PRIORITY_ABOVE_NORMAL 1 Stufe über der Prioritätsklasse des Prozesses. • THREAD_PRIORITY_HIGHEST 2 Stufen über der Prioritätsklasse des Prozesses. Es gibt zusätzlich zu diesen Möglichkeiten noch 2 weitere Parameter für die Benutzung mit der SetThreadPriority() Funktion: • THREAD_PRIORITY_IDLE setzt die Priorität des Threads auf 1, es sei denn, die Prioritätsklasse des Prozesses ist die REALTIME_PRIORITY_CLASS in diesem Fall wird die Priorität des Threads auf 16 gesetzt. • THREAD_PRIORITY_TIME_CRITICAL setzt die Priorität des Threads auf 15, es sei denn, die Prioritätsklasse des Prozesses ist die REALTIME_PRIORITY_CLASS in diesem Fall wird die Priorität des Threads auf 31 gesetzt. Dies ist die höchste Priorität und auch der einzige Weg einen Thread diese extrem hohe Priorität zuzuteilen. Um die Priorität eines laufenden Threads abzufragen benutzt man die Funktion GetThreadPriority(). Beispiel GetThreadPriority(): int prio; prio = GetThreadPriority ( hThread ); Parameter: • Handle des Thread dessen Priorität abgefragt wird. Als Rückgabewert wird die Priorität in die Variable prio geschrieben. 40 3 Betriebssystem Windows NT 4.0 3.6 Synchronisation von Threads In Windows NT gibt es wie unter anderen Betriebssystemen auch verschiedene Methoden um Threads zu synchronisieren. 3.6.1 Synchronisation durch Priorität Durch das verändern der Priorität eines Threads kann man festlegen welcher Thread innerhalb eines Prozesses die meiste Rechenzeit zur Verfügung gestellt bekommt. Eine Synchronisation zwischen den Prozessen findet aber nicht statt. Deshalb darf man streng genommen hier nicht von einer Synchronisation sprechen sondern nur von der Bevorzugung eines bestimmten Threads. Die Funktionen, SetThreadPriority() und GetThreadPriority(), die hierfür benötigt werden sind im Kapitel 3.5 genauer beschrieben. 3.6.2 Synchronisation durch kritische Bereiche Mit Hilfe von kritischen Bereichen erreicht man einen wechselseitigen Ausschluß von Threads. So das bestimmte Programmabschnitte immer nur von einem Thread ausgeführt werden können. Ein oder mehrere andere Threads die den kritischen Bereich betreten wollen müssen warten bis der Thread innerhalb des kritischen Bereiches diesen wieder verläßt (siehe Abb.3.3). Critical Section Abb. 3.3: Ausführung eines kritischen Bereiches von einem Thread [HaWi1997] Der kritische Bereich wird dabei durch eine Variable überwacht. Diese Variable wird global initialisiert, damit jeder Thread auf diese Variable zugreifen kann. 41 3 Betriebssystem Windows NT 4.0 Beispiel kritischer Bereich: CRITICAL_SECTION csOutput ; // Initialisierung der Variable um int zahl = 10; // den kritischen Bereiches zu // überwachen. void Print () { _try { // Hier versucht der Thread in den kritischen // Bereich zu gelangen indem er Zugriff auf EnterCriticalSection ( &csOutput ); // die Variable erhält. zahl -- ; cout << “Die Zahl ist “ << zahl << endl; // Innerhalb des // Bereiches wird die Variable zahl } // herabgezählt und ausgegeben. _finally { LeaveCriticalSection ( &csOutput ) ; // Am Ende des kritischen } // Bereiches wird die } // Variable wieder “freigegeben“. In diesem Beispiel hat der Einsatz des kritischen Bereiches den Vorteil, daß der selbe Thread, der die Variable “zahl“ herabzählt diese auch ausgibt ohne das ein anderer Thread die Möglichkeit besitzt die Variable vor der Ausgabe zusätzlich herabzuzählen. Kritische Bereiche werden in den Beispielprogrammen Semaphor und Philosophen eingesetzt (siehe Kapitel 6.2). 42 3 Betriebssystem Windows NT 4.0 3.6.3 Synchronisation durch Ereignisse In Windows NT ist ein Ereignis ein Synchronisationsobjekt, das von dem Betriebssystem verwaltet wird. Diesen Synchronisationsobjekten können Namen gegeben werden so das nicht nur Threads innerhalb eines Prozesses mit ihnen arbeiten können sondern auch mehrere Prozesse sich durch so ein Objekt synchronisieren können. Diese Synchronisationsart wird z.B. verwendet wenn ein Thread auf ein Ergebnis warten muß, daß von einem anderen Thread zuerst berechnet wird. Die Abbildung 3.4 zeigt einen allgemeinen Ablauf bei der Synchronisation durch Ereignisse. Thread A Thread B WaitForSingleObject( hEvent ) Thread A is blocked Thread A resumes SetEvent( hEvent ) Abb. 3.4: Grundprizip für die Verwendung von Ereignissen [HaWi1997] Ein Ereignis kann in Windows NT zwei Zustände besitzen. • signalisierend Dieser Zustand bedeutet, daß ein Thread, der eine Anfrage stellt eine Antwort erhält und so der anfragende Thread sein Programm weiter ausführen kann. • nicht signalisierend Dieser Zustand bedeutet, daß ein Thread, der eine Anfrage stellt keine Antwort erhält und so der anfragende Thread blockiert bis das das Ereignis in den Zustand signalisierend wechselt. 43 3 Betriebssystem Windows NT 4.0 Bei der Synchronisation mit Hilfe von Ereignissen muß man als erstes einen sogenannten Ereignis-Handler erstellen. Dies geschieht durch die Funktion CreateEvent(). Beispiel CreateEvent(): HANDLE hEvent = CreateEvent ( NULL, FALSE, FALSE; "EventName" ); Parameter: • Ein Pointer auf SECURITY_ATTRIBUTES-Struktur. NULL kann eingesetzt werden wenn keine Sicherheitsaspekte berücksichtigt werden müssen. • Dieses Flag wird normalerweise auf FALSE gesetzt. Es gibt an ob es sich um ein Manueller-Reset-Ereignis handelt. • Diese Flag gibt den Anfangsstatus des Ereignisses an TRUE = signalisierend FALSE = nicht signalisierend. • Hier kann man dem Ereignis einen Namen geben damit auch andere Prozesse mit diesem Ereignis arbeiten können. Man muß allerdings beachten, daß der verwendete Name nicht bereits vergeben ist. Ereignisse, Mutex und Datei-Mapping-Objekte verwenden dabei den selben Namensbereich. Ist dieser Wert NULL so kann das Ereignis nur innerhalb des Prozesses verwendet werden. Als Rückgabewert dieser Funktion erhält man einen Handle auf das Ereignis oder NULL falls ein Fehler aufgetreten ist. 44 3 Betriebssystem Windows NT 4.0 Ein anderer Prozess kann einen Handle zu dem selben Ereignis erhalten, indem er die CreateEvent() Funktion mit den selben Parameter aufruft oder die OpenEvent() Funktion benutzt. Beispiel OpenEvent(): HANDLE hEvent = OpenEvent ( EVENT_ALL_ACCESS, FALSE, “EventName“ ); Parameter: • Sicherheitsparameter der die Zugriffsrechte angibt. • EVENT_ALL_ACCESS gibt alle Zugriffsrechte frei. • EVENT_MODIFY_STATE gibt die Funktion SetEvent() und ResetEvent() frei. • SYNCHRONIZE gibt nur den Warte-Aufruf frei. • TRUE gibt an, das ein Prozess der durch CreateProzess() kreiert wird diesen Handle erben kann. Bei FALSE ist dies nicht der Fall. • Name des Ereignisses Als Rückgabewert dieser Funktion erhält man einen Handle auf das Ereignis oder NULL falls ein Fehler aufgetreten ist. Event-Handle sollte man, wenn sie nicht mehr benötigt werden, schließen. Dies geschieht mit der Funktion CloseHandle() (siehe Kapitel 3.5). Um ein Ereignis-Handle in den Zustand signalisierend zu setzen benötigt man die Funktion SetEvent(). Beispiel SetEvent(): SetEvent ( hEvent ); Parameter: • Handle des Events welches auf signalisierend gesetzt werden soll. 45 3 Betriebssystem Windows NT 4.0 Da es zwei Arten von Ereignissen gibt und zwar das Auto-Reset-Ereignis und das Manueller-Reset-Ereignis gibt es Unterschiede im Zurücksetzen dieser beiden Ereignisarten. Beim Auto-Reset-Ereignis folgt der Übergang in den nicht signalisierend Zustand automatisch nachdem eine Warte-Anfrage eines anderen Threads beantwortet wurde. Beim Manueller-Reset-Ereignis muß das Ereignis durch die Funktion ResetEvent() manuell zurückgesetzt werden. Beispiel ResetEvent(): ResetEvent ( hEvent ); Parameter: • Handle des Events welches auf nicht signalisierend gesetzt werden soll. Eine Besonderheit ist die PulseEvent() Funktion. Sie wird eingesetzt, wenn man mehrere Threads auf ein bestimmtes Ereignis warten lassen will. Da hierbei das AutoReset-Ereignis nach jeder Warte-Anfrage neu auf signalisierend gesetzt werden muß und es bei dem Manueller-Reset-Ereignis schwierig ist zu bestimmen ob nun wirklich alle wartenden Threads eine Antwort erhalten haben wird in so einer Situation die PulseEvent() Funktion verwendet. Bei dieser Funktion wird das Ereignis solange auf signalisierend gesetzt, bis alle wartenden Threads eine Antwort erhalten haben. Beispiel PulseEvent(): PulseEvent ( hEvent ); Parameter: • Handle des Events welches angesprochen werden soll. 46 3 Betriebssystem Windows NT 4.0 Es gibt zwei verschiedene Funktionen für eine Warte-Anfrage abhängig davon ob nur auf ein oder auf mehrere Ereignisse gewartet werden soll. Bei der Funktion WaitForSingleObject() wird auf das Signal eines Ereignisses gewartet. Bei der Funktion WaitForMultipleObjects() wird auf das Signal von mehreren Ereignissen gewartet. Im ersten Moment könnte man annehmen das man anstatt zwei verschiedene Funktionen zu verwenden einfach die WaitForSingleObject() Funktion mehrmals hintereinander aufrufen kann. Bei so einem Vorgehen besteht aber die große Gefahr eines Deadlocks, da sich so mehrere Threads gegenseitig sperren können (siehe Abb. 3.5). Bei der Verwendung der WaitForMultipleObjects() Funktion werden alle benötigte Ereignisse zur selben Zeit überwacht und entweder alle oder keines der Signale verwendet. Thread A Thread B Wait ForSingleObject( hMutexZ ); Wait ForSingleObject( hMutexY ); // Thread A gets control of Handle hMutexZ // waits for hMutexY // Thread B gets control of Handle hMutexY // waits for hMutexZ Wait ForSingleObject( hMutexY ); Wait ForSingleObject( hMutexZ ); Abb. 3.5: Deadlock-Situation durch geschachtelte WaitForSingleObject() [HaWi1997] Beispiel WaitForSingleObject(): DWORD dwResult = WaitForSingleObject ( hEvent, INFINITE ); Parameter: • Handle des Events, Mutex oder Semaphors welches angesprochen werden soll. • Die Zeit in Millisekunden die gewartet werden soll. INFINITE bedeutet keine Zeitbeschränkung. 47 3 Betriebssystem Windows NT 4.0 Es gibt mehrere mögliche Rückgabewerte dieser Funktion: • WAIT_OBJECT_0 wenn der Handle antwortet. • WAIT_TIMEOUT wenn die vorgegebene Zeit abgelaufen ist. • WAIT_ABANDONED wenn auf einen Mutex-Handle gewartet wird. Beispiel WaitForMultipleObjects(): HANDLE hEvents[2] DWORD dwResult = WaitForSingleObject ( 2, hEvents, TRUE, INFINITE ); Parameter: • Die Anzahl der Handles, die überprüft werden sollen. • Adresse auf das Handle-Array. • Flag, das angibt, ob alle Handles signalisieren müssen oder nur eins. TRUE bedeutet, das alle Ereignis-Handles signalisieren müssen. Bei der Verwendung von FALSE muß darauf geachtet werden das kein Deadlock entsteht (siehe oben). • Die Zeit in Millisekunden die gewartet werden soll. INFINITE bedeutet keine Zeitbeschränkung. Die Rückgabewerte dieser Funktion unterscheiden WaitForSingleObject() Funktion: sich etwas gegenüber der • WAIT_OBJECT_0 ersetzt durch WAIT_OBJECT_0 + ( Anzahl der Handles – 1 ) wird zurückgegeben, wenn der Handle das Signal gegeben hat. Der Rückgabewert bestimmt den Index des mit der niedrigsten Nummer versehenen Handles, der ein Signal bekommen soll. • WAIT_TIMEOUT wenn die vorgegebene Zeit abgelaufen ist und nicht alle Handles signalisiert haben. • WAIT_ABANDONED_0 ersetzt durch WAIT_ABANDONED_0 + ( Nummer des Handles -1) wird nur zurückgegeben, wenn ein Handle auf einen Mutex-Handle wartet. 48 3 Betriebssystem Windows NT 4.0 3.6.4 Synchronisation durch Mutex Ein Mutex ist ein Windows-NT-Objekt, das für den wechselseitigen Ausschluß verwendet wird. Mutex ist sozusagen eine Mischung aus "kritische Bereiche" und "Ereignisse". Bei einem Mutex sind die Warte-Aufrufe WaitForSingleObject() und WaitForMultipleObjects() der Threads die selben wie bei Ereignissen. Andere Funktionen für die Arbeit mit Mutex-Objekten ähneln sehr der Funktionen die bei der Arbeit mit Ereignissen verwendet werden. Ein Mutex-Objekt wird mit der Funktion CreateMutex() kreiert. Beispiel CreateMutex(): HANDLE hMutex = CreateMutex ( NULL, TRUE; "MutexName" ); Parameter: • Ein Pointer auf die SECURITY_ATTRIBUTES-Struktur. NULL kann eingesetzt werden wenn keine Sicherheitsaspekte berücksichtigt werden müssen. • Diese Flag gibt den Anfangsstatus des Mutex an TRUE = kritischer Bereich frei. • Hier kann ein Name angegeben werden um auch mit anderen Prozessen dieses Mutex zu verwenden. Man muß allerdings beachten, daß der verwendete Name nicht bereits vergeben ist. Ereignisse, Mutex und Datei-Mapping-Objekte verwenden dabei den selben Namensbereich. Ist dieser Wert NULL so kann das Ereignis nur innerhalb des Prozesses verwendet werden. Als Rückgabewert dieser Funktion erhält man einen Handle auf das Mutex oder NULL falls ein Fehler aufgetreten ist. 49 3 Betriebssystem Windows NT 4.0 Möchte nun ein Thread in einen kritischen Bereich eintreten ruft er die Funktion WaitForSingleObject() oder WaitForMultipleObjects() auf. Ist der kritische Bereich frei so wird der Programmteil ausgeführt. Am Ende des kritischen Bereiches muß der Thread den Mutex wieder "frei" geben. Dies geschieht mit der Funktion RelaseMutex(). Wird der Zugriff eines kritischen Bereiches durch mehrere Mutexe gesteuert muß die Funktion RelaseMutex() sooft aufgerufen werden, bis alle Mutexe wieder freigegeben sind. Beispiel RelaseMutex(): RelaseMutex ( hMutex); Parameter: • Handle des Mutex welches wieder freigegeben werden soll. Wie bei Ereignissen können auch Mutexe, wenn sie einen Namen haben, von anderen Prozessen benutzt werden. Dies geschieht durch die Funktion OpenMutex(). Beispiel OpenMutex(): HANDLE hMutex = OpenMutex ( MUTEX_ALL_ACCESS, FALSE, “MutexName“ ); Parameter: • Sicherheitsparameter der die Zugriffsrechte angibt. MUTEX_ALL_ACCESS gibt alle Zugriffsrechte frei. • TRUE gibt an, das ein Prozess der durch CreateProzess() kreiert wird diesen Handle erben kann. Bei FALSE ist dies nicht der Fall. • Name des Ereignisses Mutex-Handel sollte man, wenn sie nicht mehr benötigt werden, schließen. Dies geschieht mit der Funktion CloseHandle() (siehe Kapitel 3.5). Die Anwendung eines Mutex wird im Beispielprogramm Philosophen (Kapitel 6.2.7) gezeigt. 50 3 Betriebssystem Windows NT 4.0 3.6.5 Synchronisation durch Semaphore Semaphore arbeiten in Windows NT nach dem selben Prinzip wie in Unix und Java. Es sind sozusagen Zähler dessen Werte erhöht oder herabgezählt werden je nachdem welche Operation auf so einen Semaphor einwirkt. Ist der interne Zähler NULL muß ein Thread warten, wenn er mit seiner Operation den internen Zähler herabsetzen will. (Ein kleines Beispiel für die Arbeitsweise eines Semaphores ist im Kapitel 4.6.1 beschrieben.) Als erstes muß ein Semaphor mit der Funktion CreateSemaphor() erzeugt werden damit später die Threads mit ihm arbeiten können. In Windows NT wird der Zugang zu einem kritischen Bereich, auch bei der Verwendung eines Semaphores zur Synchronisation, durch die schon oben erläuterte WaitForSingleObject() oder bei der Verwendung von mehreren Semaphoren durch die WaitForMultipleObjects() Funktion erreicht. Wie bei der Synchronisation durch Mutex muß hier auch beim Verlassen des kritischen Bereiches ein Funktionsaufruf erfolgen, der in diesem Fall den internen Zähler des Semaphores wieder erhöht. Diese Funktion lautet ReleaseSemaphore(). Wie bei den anderen Synchronisationsarten sollte auch hier der Handle des Semaphores durch die Funktion CloseHandle() (siehe Kapitel 3.5) geschlossen werden. Damit andere Prozesse auf ein durch CreateSemaphor() erstelltes Semaphor zugreifen können benötigt man auch hier eine “Open“ Funktion und zwar die OpenSemaphore() Funktion. Beispiel CreateSemaphor(): HANDLE semaphor = CreateSemaphore ( NULL, 3, 3, “SemapName“ ); Parameter: • Ein Pointer auf SECURITY_ATTRIBUTES-Struktur. NULL kann eingesetzt werden wenn keine Sicherheitsaspekte berücksichtigt werden müssen. • Der Anfangswert, den das Semaphor besitzen soll. Dieser Wert muß größer oder gleich NULL sein. • Der Maximalwert, den das Semaphor annehmen darf. Dieser Wert muß gleich oder größer sein als der angegebene Anfangswert. Mindestwert ist hier 1. • Hier kann man dem Semaphor einen Namen geben damit auch andere Prozesse mit diesem Semaphor arbeiten können. Man muß allerdings beachten, daß der verwendete Name nicht bereits vergeben ist. Ereignisse, Mutex und Datei-MappingObjekte verwenden dabei den selben Namensbereich. Ist dieser Wert NULL so kann das Ereignis nur innerhalb des Prozesses verwendet werden. Als Rückgabewert dieser Funktion erhält man einen Handle auf das Semaphor oder NULL falls ein Fehler aufgetreten ist. 51 3 Betriebssystem Windows NT 4.0 Beispiel OpenSemaphore(): HANDLE semaphor = OpenSemaphore ( SEMAPHORE_ALL_ACCESS, NULL, “SemapName“ ); Parameter: • Sicherheitsparameter der die Zugriffsrechte angibt. • SEMAPHORE_ALL_ACCESS gibt alle Zugriffsrechte frei. • SEMAPHORE_MODIFY_STATE gibt die Funktion ReleaseSemaphore() frei. • SYNCHRONIZE gibt nur den Warte-Aufruf frei. • TRUE gibt an, das ein Prozess der durch CreateProzess() kreiert wird diesen Handle erben kann. Bei FALSE ist dies nicht der Fall. • Name des Semaphores. Als Rückgabewert dieser Funktion erhält man einen Handle auf das Semaphor oder NULL falls ein Fehler aufgetreten ist. Beispiel ReleaseSemaphore(): ReleaseSemaphore ( semaphor, 1, NULL ); Parameter: • Handle des Semaphores. • Wert, um den das Semaphor erhöht werden soll. • Hier kann man wenn es benötigt wird einen Pointer auf eine long-Variable angeben damit man den vorherigen Wert des internen Zählers erhält. Wird der vorherige Wert nicht benötigt kann hier NULL angegeben werden. Diese Funktion gibt als Rückgabewert TRUE zurück wenn kein Fehler auftritt ansonsten wird FALSE zurückgegeben. 52 3 Betriebssystem Windows NT 4.0 3.7 Kommunikation In Windows NT gibt es, wie bei den anderen Umgebungen mehrere Arten der Kommunikation. Die Kommunikation zwischen Prozessen bzw. Threads findet mit der Hilfe von Pipes statt. Daher wird diese Art der Kommunikation hier näher erläutert. Eine Pipe ist dabei ein Kommunikationskanal wodurch die Prozesse bzw. Threads miteinander verbunden sind. In Windows NT gibt es zwei Grundtypen von Pipes unbenannte Pipes (Kapitel 3.7.1) und benannte Pipes (Kapitel 3.7.2). Beide Pipetypen benutzen zur Kommunikation die standardmäßigen Dateiverwaltungsfunktionen von Windows NT. ReadFile() zum Auslesen der Pipe und WriteFile() zum Schreiben in die Pipe. Die Funktionen ReadFileEx() und WriteFileEx() die ausschließlich für asynchronen Datei-In bzw. Output verwendet werden können auch für Pipes verwendet werden. In dieser Diplomarbeit werden diese beiden Funktionen nicht näher erläutert. Beispiel ReadFile(): BOOL fRead = ReadFile ( hRead, szBuffer, sizeof(szBuffer), &dwRead, NULL ); if ( fRead == FALSE || dwRead == 0 ) { // Fehlermeldung } Parameter: • Datei-Handler, von dem gelesen werden soll. • Adresse des Puffers, der die Daten aufnehmen soll. • Die maximale Anzahl der auszulesenden Bytes. • Ein Pointer auf ein DWORD. Dort wird die Anzahl der tatsächlich ausgelesenen Bytes gespeichert. • Optionale OVERLAPPED-Struktur für asynchrones lesen. NULL wenn diese Option nicht genutzt werden soll. Dieser Parameter wird bei einer unbenannten Pipe von Windows NT ignoriert. Diese Funktion gibt TRUE zurück falls kein Fehler aufgetreten ist ansonsten FALSE. 53 3 Betriebssystem Windows NT 4.0 Beispiel WriteFile(): BOOL fWritten = WriteFile ( hWrite, szBuffer, sizeof(szBuffer), &dwWritten, NULL ); if ( fWritten == FALSE || dwWritten == 0 ) { // Fehlermeldung } Parameter: • Datei-Handler, der beschrieben werden soll. • Adresse des Puffers, der die zu schreibenden Daten enthält. • Die maximale Anzahl der zu schreibenden Bytes. • Ein Pointer auf ein DWORD. Dort wird die Anzahl der tatsächlich geschriebenen Bytes gespeichert. • Optionale OVERLAPPED-Struktur für asynchrones lesen. NULL wenn diese Option nicht genutzt werden soll. Dieser Parameter wird bei einer unbenannten Pipe von Windows NT ignoriert. Diese Funktion gibt TRUE zurück falls kein Fehler aufgetreten ist ansonsten FALSE. 54 3 Betriebssystem Windows NT 4.0 3.7.1 Unbenannte Pipes Dieser Type von Pipe wird zur Kommunikation zwischen zusammenhängenden Prozessen auf einem Rechner eingesetzt wie z.B. zwischen Mutter- und Kindprozess. Sie können nicht für die Kommunikation in Netzwerken verwendet werden. Eine unbenannte Pipe arbeitet unidirektional, so daß es immer ein “Leseende“ und ein “Schreibende“ gibt. Möchte man zwischen einem Mutter- und einem Kind-Prozess in beide Richtungen kommunizieren, so benötigt man zwei unbenannte Pipes. Eine unbenannte Pipe wird mit der Funktion CreatePipe() erzeugt. Eine Pipe wird solange nicht geschlossen, bis alle offene Handles, auch die geerbten Handles, geschlossen sind. Mit der Funktion CloseHandle() (siehe Kapitel 3.5) werden die einzelnen Handles der Pipe geschlossen. Beispiel CreatePipe(): BOOL fCreated = CreatePipe ( &hRead, &hWrite, NULL, 0 ); if ( !fCreated ) // Fehlermeldung Parameter: • Die Adresse der Variable, die den Read-Handle speichern soll. • Die Adresse der Variable, die den Write-Handle speichern soll. • Die Adresse einer SECURITY_ATTRIBUTES-Struktur oder NULL für den StandardSicherheitsdeskriptor. • Die Puffergröße der Pipe oder 0 für die Standardgröße. Diese Funktion gibt TRUE zurück falls kein Fehler aufgetreten ist ansonsten FALSE. Soll ein Kind-Prozess die Handles einer unbenannten Pipe erben so benötigt man eine SECURITY_ATTRIBUTES-Struktur, da der Standard Sicherheitsdeskriptor eine Vererbung der Pipe-Handles nicht zuläßt. Beispiel für die Verwendung einer SECURITY_ATTRIBUTES-Struktur damit die PipeHandles vererbt werden können: SECURITY ATTRIBUTES sec; sec.nLength = sizeof( SECURITY ATTRIBUTES ); sec.lpSecurityDescriptor = NULL; sec.bInheritHandle = TRUE; CreatePipe ( &hRead, &hWrite, &sec, 0 ); 55 3 Betriebssystem Windows NT 4.0 3.7.2 Benannte Pipes Benannte Pipes besitzen einige Vorzüge gegenüber unbenannte Pipes. Benannte Pipes können nicht nur durch Handles sondern auch durch einen Namen angesprochen werden. Sie können im Gegensatz zu unbenannten Pipes auch zur Kommunikation innerhalb eines Netzwerkes verwendet werden. Eine benannte Pipe kann zur Kommunikation in zwei Richtungen eingesetzt werden. Ihre Verbindungsoptionen sind flexibler als die von unbenannten Pipes. Eine benannte Pipe unterstützt zudem asynchrones, überlappendes I/O welches bei einer unbenannten Pipe von Windows NT ignoriert wird selbst wenn der dafür erforderliche Parameter angegeben wird. Wie eine unbenannte Pipe wird eine benannte Pipe solange nicht geschlossen, bis alle offene Handles geschlossen sind. Mit der Funktion CloseHandle() (siehe Kapitel 3.5) werden die einzelnen Handles der Pipe geschlossen. Der Funktionsaufruf einer benannten Pipe ist CreateNamedPipe(). Damit ein anderer Prozess oder Thread mit dieser Pipe arbeiten kann muß er die Funktion CreateFile() aufrufen damit er einen Handle für die Pipe erhält. Wird eine Pipe von anderen Prozessen benutzt muß der Server-Prozess die Pipe durch ConnectNamedPipe() in den "Empfangen-Modus" versetzen bevor eine andere Anwendung sie gebrauchen kann (siehe Kapitel 6.2.12 "Benannte Pipe für zwei Prozesse). Benannte Pipes kann man darüber hinaus in verschiedene Arten unterteilen. Ein wesentliches Merkmal ist dabei der Modus der angibt wie Lese- oder Schreib-Anfragen bearbeitet werden. Beim "Non-Blocking" Modus wird eine Lese- oder Schreib-Anfragen solange fortgesetzt, bis die Pipe diese Operation beendet hat. Diese Pipes werden auch "Polling"-Pipes genannt. Meistens werden Pipes im "Blocking"-Modus mit implementierten asynchronen Lese- und Schreib- Operationen geöffnet. Benannte Pipes können als Byte-Pipe oder Nachrichten-Pipe geöffnet werden. Bei einer Byte-Pipe werden die Daten einfach als Byte Strom in die Pipe geschrieben. Bei einer Nachrichten-Pipe werden die Daten beim Schreiben in Pakete unterteilt. Diese Pakete können dann als Datenstücke gelesen werden. Wird eine Pipe als Byte-Pipe erzeugt, so kann aus ihr nur als Byte-Pipe gelesen werden. Wird eine Nachrichten-Pipe erzeugt, so kann aus ihr als Byte-Pipe oder als NachrichtenPipe gelesen werden. Bei der Verwendung der "write-through" Option wartet eine Pipe auf das Ergebnis einer Schreib-Operation bis die Daten übermittelt wurden. Eine Nachrichten-Pipe hat diese Option immer, während sie bei einer Byte-Pipe eingestellt werden muß. 56 3 Betriebssystem Windows NT 4.0 Beispiel CreateNamedPipe(): HANDLE hPipe = CreateNamedPipe ( TEXT("\\\\.\\pipe\\pipe_name"), PIPE_ACCESS_DUBLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE, PIPE_UNLIMITED_INSTANCES, 4096, 4096, INFINITE, NULL ); Parameter: • Name der Pipe. Der Name einer benannten Pipe hat folgendes Format: \\machine_name\pipe\pipe_name. Ein Punkt anstelle des machine_name bedeutet, daß die Pipe auf den eigenen Rechner verweist. Bei Pipenamen unterscheidet Windows NT nicht zwischen Groß- und Kleinschreibung. • Lese-, Schreib-, Sicherheitsattribute für die Pipe. • ACCESS_SYSTEM_SECURITY Legt fest, daß die Client-Seite der Pipe Schreibzugriff auf das ACL-System der benannten Pipe haben darf. • FILE_FLAG_OVERLAPPED Legt fest, ob Lese-, Schreibe- und Verbindungsoperationen asynchron mit einer OVERLAPPED-Struktur ausgeführt werden soll. • FILE_FLAG_WRITE_THROUGH Zeigt an, daß der "Write-Through"-Modus eingeschaltet ist. • PIPE_ACCESS_DUBLEX Legt eine bidirektionale Pipe an. • PIPE_ACCESS_INBOUND Legt fest, daß die Pipe für den eingehenden Datenverkehr auf der Server-Seite der Pipe verwendet werden soll. • PIPE_ACESS_OUTBOUND Legt fest, daß die Pipe für den ausgehenden Datenverkehr auf der Server-Seite der Pipe verwendet werden soll. • WRITE_DAC Zeigt an, daß die Client-Seite der Pipe Schreibzugriff auf die "Discretionary Access Control List" der benannten Pipe hat. • WRITE_OWNER Zeigt an, daß die Client-Seite der Pipe Schreibzugriff auf den Eigentümer der benannten Pipe hat. 57 3 Betriebssystem Windows NT 4.0 • Der Pipe-Modus und die "Blocking" Art der Pipe • PIPE_WAIT Zeigt, daß der "Blocking" Modus eingeschaltet ist. • PIPE_NOWAIT Zeigt, daß der eingeschaltet ist. • PIPE_READMODE_BYTE Daten werden als Bytefluß aus der Pipe gelesen. • PIPE_READMODE_MESSAGE Daten werden als Nachrichtenfluß aus der Pipe gelesen. • PIPE_TYPE_BYTE Daten werden als Bytefluß in die Pipe geschrieben. • Daten werden als Nachrichtenfluß in die Pipe geschrieben. PIPE_TYPE_MESSAGE "Non-Blocking" Modus • Die Anzahl der möglichen Instanzen der Pipe. UNLIMITED_PIPE_INSTANCES für eine uneingeschränkte Anzahl. • Anzahl der Byte die für den Ausgabepuffer reserviert werden soll. • Anzahl der Byte die für den Eingabepuffer reserviert werden soll. • Time-Out-Zeit der Pipe in Millisekunden. INFINITE bedeutet kein Zeitlimit. • Pointer auf eine SECURITY_ATTRIBUTES-Struktur. Bei NULL benutzt der Pipe-Handle den Sicherheitsdeskriptor des aktuellen Zugriffs-Tokens. Als Rückgabewert dieser Funktion erhält man einen Handle auf die erzeugte Pipe oder INVALIDE_HANDLE_VALUE, wenn die Pipe nicht erstellt werden konnte. 58 3 Betriebssystem Windows NT 4.0 Beispiel ConnectNamedPipe(): BOOL fConnected = ConnectNamedPipe ( hPipe, NULL ); if ( fConnected || GetLastError() == ERROR_PIPE_CONNECTED ) { // Mit einer Anwendung verbunden } else { // Fehlermeldung } Parameter: • Handle der Pipe. • Die Adresse einer OVERLAPPED-Struktur oder NULL. Der Rückgabewert dieser Funktion ist TRUE, wenn ein Client erfolgreich mit der Pipe verbunden wurde. Tritt ein Fehler auf wird FALSE zurückgegeben. Versucht ein Client eine Pipe zu kontaktieren die zwar erzeugt wurde aber bei der der Server-Prozess der Pipe noch nicht die ConnectNamedPipe() Funktion aufgerufen hat so ist der Rückgabewert FALSE und GetLastError() gibt ERROR_PIPE_CONNECTED zurück. 59 3 Betriebssystem Windows NT 4.0 Beispiel CreateFile(): HANDLE hPipe = CreateFile ( TEXT("\\\\.\\pipe\\pipe_name"), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL ); if ( hPipe == INVALID_HANDLE_VALUE ) { // Fehlermeldung } Parameter: • Vollständiger Pipe Name. • Lese- und Schreibzugriff für den Pipe-Handle. • Der "share"-Modus. Bei benannten Pipes immer 0. • Pointer auf eine SECURITY_ATTRIBUTES-Struktur. Bei NULL benutzt der Pipe-Handle den Sicherheitsdeskriptor des aktuellen Zugriffs-Tokens. • Die "Handle-Creation"-Information- Bei benannten Pipes immer OPEN_EXISTING. • Flag für Dateiattribute. Bei benannten Pipes immer 0. • Die Adresse einer OVERLAPPED-Struktur, wenn die Pipe im OVERLAPPED-Modus geöffnet wurde ansonsten NULL. Als Rückgabewert diese Funktion erhält man einen Handle auf die geöffnete Pipe oder INVALIDE_HANDLE_VALUE, wenn die Pipe nicht erstellt werden konnte. 60 3 Betriebssystem Windows NT 4.0 Öffnet man einen Pipe-Handle mit der Funktion CreateFile() so wird dieser PipeHandle als Byte-Pipe geöffnet. Möchte man jedoch eine Pipe als Nachrichten-Pipe verwenden so muß die Funktion SetNamedPipeHandleState() angewendet werden. Beispiel SetNamedPipeHandleState(): DWORD dwPipeState = PIPE_READMOD_MESSAGE; BOOL fChangedState = SetNamedPipeHandleState ( hPipe, &dwPipeState, NULL, NULL, ); Parameter: • Handle der benannten Pipe • Ein Pointer auf die Variable in der der neue Pipe Mode steht. • Ein Pointer auf eine Variable in der die maximale Abzahl der Bytes steht die vor dem Übermitteln der Daten vom Client Rechner zum Server angesammelt werden darf. Hier kann auch NULL angegeben werden, wenn dieser Parameter nicht berücksichtigt werden soll. • Ein Pointer auf eine Variable wo eine maximale Zeit in Millisekunden angegeben wird. Innerhalb dieser Zeit müssen Daten über das Netzwerk zum Server übermittelt werden. NULL muß hier angegeben werden, wenn der Pipe Handle das "Server-Ende" der Pipe ist oder der Server- und Client-Prozess auf dem selben Rechner sind. NULL kann verwendet werden, wenn der vorherige Parameter für die maximale Anzahl der Bytes (s.o.) auch NULL ist. Der Rückgabewert dieser Funktion ist TRUE wenn sie erfolgreich ausgeführt wurde. Bei einem Fehler wird FALSE zurückgegeben. 61 4 Betriebssystem Unix / Linux 4 Betriebssystem Unix / Linux 4.1 Grundlegende Eigenschaften Unix und Linux sind Betriebssysteme, die auf der gesamten Bandbreite der heutigen Computer eingesetzt werden können. Im PC Bereich und auf Kleincomputer (PocketPC‘s) wird häufig Linux verwendet. Beim gewerblich genutzten Computer (Großrechner) wird Unix verwendet. Unix und Linux sind Allzweck-Systeme. Sie sind geeignet für die Entwicklung von Systemund Anwendersoftware. Durch eine verständliche und auch oft einfache Programmierschnittstelle ist das Schreiben von Programmen, insbesondere von Anfängern und Privatpersonen, einfacher, als z.B. unter Windows NT. Beide Betriebssysteme, Unix und Linux, unterstützen Mehrbenutzer- und Mehrprogrammbetrieb. Sie besitzen beide ein geräteunabhäniges hierarchisches Dateisystem. Dienstprogramme, für beide Systeme, sowie Treiber für fast jede Hardware, sind heutzutage in großer Anzahl erhältlich. Linux ist frei erhältlich. Für umfangreiche Komplettpakete (z.B. von SuSE) muß nur ein Bruchteil bezahlt werden, von dem was andere Betriebssysteme kosten. Linux erfreut sich im Privatbereich immer gößerer Verbreitung, da es eine günstige und vollständige Alternative ist. 4.2 Prozesse / Threads in Unix / Linux Die beiden Betriebssyteme Unix und Linux sind Multitasking-Systeme. Dies bedeutet, daß mehrere Anwendungen und Hintergrundprozesse parallel aktiv sein können. Es ist aber nicht immer möglich, daß eine Anwendung (Prozess) selbst aus mehreren "parallel" ablaufenden Teilen besteht. Auf klassischen Unix-Systemen werden derartige Probleme so gelöst, daß ein Programm mehrere Prozesse erzeugt, die dann in einer eigenen Umgebung, mit einem eigenen Speicherbereich etc. ablaufen und über die üblichen Inteprozesskommunikationsprimitiven miteinander kommunizieren. Diese Prozesse werden Kind-Prozesse genannt. Die Verwaltung solcher Kind-Prozesse kostet jedoch Zeit und Systemressourcen. In Linux kann man, da die Kind-Prozesse einer Anwendung inhaltlich eng aneinander gebunden sind und auch auf gemeinsame Variablen zugreifen sollten, die sogenannten Threads (Light-Weight-Processes) verwenden. Threads unter Linux kann man gemeinsame Speicherbereiche zuteilen. Bestimmte Speicherbereiche können als sog. "Shared Memory" vereinbart werden. System intern wird in Linux allerdings kein Unterschied zwischen einem Thread und einem Prozess gemacht. Ein Thread ist für Linux ein Prozess, der seinen Kontext ganz oder teilweise mit einem anderen Prozess teilt. 62 4 Betriebssystem Unix / Linux In dieser Diplomarbeit wird zum "parallelen" Abarbeiten von Programmteilen die klassische Methode, die Erzeugung von mehreren Kind-Prozessen, besprochen. Diese Art kann sowohl bei allen gängigen Unix-Systemen als auch bei einem Linux-System angewendet werden. 4.3 Zustände von Prozessen Abb. 4.1: Zustandsdiagramm von Prozessen in Unix / Linux [Vogt 2001] Während seiner Existenz kann ein Prozess in Unix / Linux mehrere Zustände annehmen. Ein Zustandswechsel wird dabei durch ein Ereignis ausgelöst. Der Zustand rechnend besagt, daß der Prozess gerade von der CPU bearbeitet wird. Der Zustand bereit besagt, daß der Prozess zwar bereit ist, aber auf die Benutzung der CPU wartet. Der Zustand blockiert besagt, daß der Prozess auf das Eintreten eines bestimmten Ereignisses wartet (z.B. Benutzereingabe) bevor er weiter ausgeführt werden kann. Bei Unix / Linux gibt es neben den oben genannten Zuständen eine etwas abgewandelte Version. Der Zustand rechnend wird aufgeteilt in einen User Mode und einen Kernel Mode. Der Übergang zwischen diesen beiden Zuständen erfolgt durch den Aufruf einer Systemfunkion bzw. die Rückkehr von derselben. Man beachte, daß der Übergang in einen anderen Zustand nur aus dem Kernel Mode möglich ist und nicht aus dem User Mode. Ein weiterer Zustand ist Zombie. In diesen Zustand gelangen Prozesse, wenn z.B. ein Sohnprozess terminiert und der Vater noch keinen wait() Aufruf durchgeführt hat, da erst dann ein Prozess vollständig gelöscht wird. Terminiert ein Vaterprozess ohne wait() Aufruf, so erbt der Init-Prozess diese Prozesse und sorgt durch wait() Aufrufe für die vollständige Löschung dieser Zombies nachdem sie terminiert sind. 63 4 Betriebssystem Unix / Linux 4.4 Priorität und Scheduling Es werden zwei Arten von Scheduling unterschieden, unterbrechendes und nicht unterbrechendes Scheduling. Unix und Linux benutzen ein unterbrechendes Scheduling, so das ein Prozess vom Betriebssystem unterbrochen werden kann, damit ein anderer Prozess von der CPU ausgeführt wird. Beide Betriebsysteme arbeiten allerdings mit unterschiedlichen Systemen. Unix benutzt ein prioritätgesteuertes Scheduling bei dem die Priorität der einzelnen Prozesse vom System anhand mehrerer Faktoren in bestimmten Zeitabständen berechnet wird. Diese Faktoren sind z.B. wie rechenintensiv der Prozess bis zu diesem Zeitpunkt war oder wie lange der Prozess schon warten mußte. Der nice Wert, dem man beim Starten eines Prozesses vorgeben kann wird ebenfalls in diese Berechnung einbezogen. In Unix bedeutet i.a. ein höherer Prioritätswert eine niedrige “Dringlichkeit“. Linux verwendet eine Mischung aus verschiedenen Schedulingklassen. Diese Klassen sind: 1. SCHED_FIFO: Bei Prozessen der selben Priorität wird der älteste Prozess als erstes bearbeitet. 2. SCHED_RR: Bei Prozessen der selben Priorität wird durch eine Zeitscheibe bestimmt wie lange jeder Prozess die CPU erhält. 3. SCHED_OTHER: In dieser Klasse wird durch eine Rechenformel die Reihenfolge bestimmt. In dieser Formel wird z.B. die CPU-Zeit, die der Prozess bereits erhalten hat berücksichtigt. Linux unterscheidet hierbei zwei Klassen von Prozessen dringende “Realzeitprozesse“ und weniger dringende “Prozesse“. Realzeitprozesse besitzen eine Realzeitpriorität, die entscheidet in welcher Reihenfolge die Prozesse abgearbeitet werden. Je größer dieser Realzeitprioritäswert, desto “dringender“ ist dieser Prozess. Diese Prozesse werden in der SCHED_FIFO und SCHED_OTHER Klasse verwaltet. Weniger “dringende“ Prozesse erhalten die Realzeitpriorität 0 und werden in der Schedulingklasse SCHED_OTHER verwaltet. Diese Prozesse erhalten erst dann Rechenzeit, wenn keine Realzeitprozesse bearbeitet werden müssen [Vogt2001]. Obwohl beide Betriebssysteme unterschiedliche Methoden anwenden haben sie das gleiche Ziel. Sie versuchen alle Prozesse so zu koordinieren, das auch, wenn das System ausgelastet ist, möglichst jeder Prozess, auch wenn es ein weniger wichtiger ist, die CPU zugeteilt bekommt wenn dadurch “dringende“ Prozesse nicht behindert werden. 64 4 Betriebssystem Unix / Linux 4.5 Erzeugung und Steuerung von Prozessen Prozesse werden in Unix erzeugt, indem ein bestehender Prozess die Funktion fork() aufruft. Das gilt nicht für Prozesse, die vom Systemkern automatisch beim Hochlaufen des Systems erstellt werden, wie z.B. den Swapper. Start fork()-Return im Vaterprozess wait() fork()-Return im Sohnprozess exit() fork() hier laufen die Prozesse asynchron Abb. 4.2: Erzeugen eines Sohnprozesses und Warten auf dessen Beendigung [GuOb1995] Der neue, von fork() kreierte Prozess, wird Kindprozess genannt. Er ist eine Kopie des aufrufenden Prozesses und erhält z.B. eine Kopie des Datenbereiches des Elternprozesses. Die beiden Prozesse teilen sich aber diese Speicherbereiche nicht. Die Funktion wird nur einmal aufgerufen, liefert aber zwei Rückgabewerte. Dem Kindprozess wird der Wert 0, dem Elternprozess dagegen die Prozessidentifikationsnummer (PID) des Kindprozesses übergeben. Die PID des Kindprozesses wird dem Elternprozess übergeben, da ein Elternprozess über mehrere Kindprozesse verfügen kann. Es gibt keine zusätzliche Funktion, mit der ein Elternprozess die PID der Kindprozesse ermitteln kann. Ein Kindprozess kann aber immer nur einen Elternprozess haben, daher erhält der Kindprozess immer den Rückgabewert 0. Da fork() eine genaue Kopie des aufrufenden Prozesses erzeugt und startet, ist die PID der einzige Unterschied. Anhand der unterschiedlichen PID's, die Vater- und Sohnprozess besitzen, können sie in verschiedene Programmstücke verzweigen. Ein Prozess beendet sich selber, wenn er das Ende des Programmcodes erreicht oder die Schnittstelle exit() aufruft. Bei dem Aufruf exit() wird ein ganzzahliger Parameter (z.B. als Fehlercode) an den Vaterprozess übergeben. Prozesse können aber auch durch andere Prozesse mit Hilfe von Signalen beeinflußt werden, falls sie die Berechtigung dazu haben. Die Schnittstellenfunktion hierfür lautet kill(). Signale können auch mit der Funktion pause() zur Synchronisation eingesetzt werden (siehe Kapitel 4.7.2 Synchronisation durch Signale). 65 4 Betriebssystem Unix / Linux Einige Schnittstellenfunktionen im Zusammenhang mit Prozessen sind: execv Aufruf eines Programmes aus einem Prozess Prototyp: int execv ( char *pfad, char *argv[ ] ); Parameter: char *pfad Name bzw. Pfad des Programms char *argv[ ] Zeiger auf das Feld, in dem die Argumente für das aufgerufene Programm stehen. Das letzte Element muss ein NULL-Pointer sein. exit Beendet den Prozess. Prototyp: void exit ( int status ); Parameter: int status Beispiel: exit(0); fork Starten einen neuen Prozess. Prototyp: int fork ( ); Rückgabe: 0 an Kindprozess, Prozessidentifikationsnummer (PID) des Kindes an Elternprozess, -1 bei Fehler. Beispiel: int kind_PID; if ((kind_PID= fork()) == 0) { ** Programmcode wird vom Sohnprozess ausgeführt ** } getpid Liefert die Prozessidentifikationsnummer (PID) des aufrufenden Prozesses bzw. -1 bei einem Fehler. Prototyp: int getpid ( ); Beispiel: printf("Meine PID ist: %i\n",getpid()); /* Gibt die eigene PID aus */ Status der zurückgegeben wird (0 = OK) /* Fehlerfreie Terminierung des Prozesses */ 66 4 Betriebssystem Unix / Linux getppid liefert die Prozessidentifikationsnummer des Vaterprozesses (PPID) bzw. -1 bei einem Fehler. Prototyp: int getppid ( ) ; Beispiel: printf("Die PID meines Vaters ist: %i\n",getppid()); /* Gibt PID des Vaters aus */ kill Das Signal sig wird durch diese Funktion an den Prozess mit der Prozessidentifikationsnummer pid geschickt. Includes : #include <signal.h> Prototyp: int kill ( int pid, int sig ) ; Parameter: int pid PID des Empfänger-Prozesses int sig Beispiel: Signalnummer SIGKILL bzw. 9 terminiert den Prozess, ohne daß er die Möglichkeit hat, das Signal abzufangen. Andere Signale wie z.B. SIGUSR1 können vom Empfängerprozess abgefangen werden und bewirken eine Ausführung des "Signal-Handlers" eine vom Benutzer definierte Funktion. int kind_PID; /* terminiert den Prozess,*/ kill (kind_PID,SIGKILL); /* dessen PID in der */ /* Variable kind_PID gespeichert ist. */ Beispiel zur Synchronisation: siehe Kapitel 4.7.2 Synchronisation durch Signale. pause Der Prozess wird angehalten und wartet auf ein Signal. Prototyp: int pause ( ); Beispiel: siehe Synchronisation durch Signale. signal Diese Funktion bindet das Signal sig an einen Signal Handler. Includes : #include <signal.h> Prototyp: void signal ( int sig, int *sighand ); Parameter: int sig Signal, welches an den Signal Handler gebunden werden soll. int *sighand Signal Handler der ausgeführt werden soll. Beispiel: siehe Synchronisation durch Signale. 67 4 Betriebssystem Unix / Linux sleep Der aufrufende Prozess blockiert für eine bestimmte Zeit. Prototyp: unsigned sleep ( int sec ); Parameter: int sec Dauer in Sekunden. Beispiel: sleep(10); wait Es wird auf die Beendigung eines Sohnprozesses gewartet. Hat ein oder mehrere Sohnprozesse bereits terminiert, so kehrt der Aufruf sogleich zurück. Ist dies nicht der Fall wird auf die Beendigung des nächsten Sohnprozesses gewartet. Prototyp: int wait ( int *statusp ); Parameter: int *statusp Zeiger auf Variable, in der der Terminierungsstatus des Sohnes zurückgegeben wird. Benötigt man den Rückgabestatus nicht, kann 0 als Parameter benutzt werden. Rückgabe: PID des terminierten Sohnes bzw. -1 wenn kein Sohnprozess existiert oder bereits terminierte Söhne durch frühere wait() Aufrufe entgegengenommen wurden. waitpid Es wird auf die Beendigung eines bestimmten Sohnprozesses gewartet. Prototyp: int waitpid ( int pid, int * statusp, int optionen ); Parameter: int *statusp siehe wait() Aufruf int pid pid > 0, PID des Prozesses auf den gewartet werden soll. Bei -1 wird auf die Beendigung eines beliebigen Sohnprozesses gewartet. int optionen Der Parameter bestimmt wie und worauf gewartet werden soll. Er ist aber abhänig davon ob das System z.B. eine Job-Kontrolle unterstützt. Beispiel: /* Prozess blockiert 10 Sekunden*/ watipid(pid,0,0); /* Prozess wartet bis der */ /* Sohnprozess mit der PID = pid terminiert */ 68 4 Betriebssystem Unix / Linux 4.6 Hintergrundprozesse "Dämons" Dämons werden häufig schon beim Laden des Systems gestartet und erst beim Systemabschluß beendet. Sie laufen sozusagen im Hintergrund, da sie über kein steuerndes Terminal verfügen. Sie werden häufig für Systemaktivitäten benötigt, die nebenläufig zu Benutzerprogrammen ablaufen z.B. warten auf Mails. Ein Dämon entsteht auch, wenn durch fork() ein Sohnprozess gestartet wurde und der Eltenprozess beendet wird bevor der Sohnprozess terminiert. Dies kann mit dem Aufruf von wait() vor dem Beenden des Vaterprozesses verhindert werden. 4.7 Synchronisation In Unix / Linux gibt es verschiedene Arten und Methoden, um Prozesse zu synchronisieren. Die wichtigsten Methoden werden hier genauer besprochen. 4.7.1 Synchronisation durch Semaphore Ein Semaphor ist ein Zähler der mehreren Prozessen gleichzeitig den Zugriff auf ein gemeinsames Datenobjekt ermöglicht. Es wird geregelt, wieviele Prozesse gleichzeitig eine gemeinsame Ressource benutzen dürfen. Ein Beispiel aus dem Leben ist z.B. die Benutzung eines Aufzuges, in dem maximal 5 Personen Platz haben. Ein Semaphor wäre hier ein Zähler, der auf 5 initialisiert wird und jedesmal, wenn eine Person den Aufzug betritt, wird er um 1 herabgezählt (dekrementiert). Verlässt eine Person den Aufzug, dann wird der Zähler um 1 erhöht (inkrementiert). Ist der Zähler auf 0, so darf keine weitere Person den Aufzug betreten. Um eine gemeinsam genutzte Ressource zu verwenden, muß ein Prozess Schritte ausführen: folgende 1. Der Semaphor, der die Ressource kontrolliert muß abfragt werden. 2. Wenn der Semaphor einen Wert größer als 0 hat dekrementiert der Prozess den Semaphor um 1 und verwendet die Ressource. Wenn der Semaphor einen Wert von 0 hat wird der Prozess angehalten. Ein anderer Prozess, der zur Zeit die Ressource benutzt, muß zuerst den Wert des Semaphores beim Verlassen der Ressource um 1 inkrementieren. Jetzt kann der wartende Prozess fortfahren mit Schritt 1. 3. Beim Verlassen der Ressource wird der Semaphor um 1 inkrementiert. Eventuell wartende Prozesse werden jetzt fortgesetzt, die wieder mit Schritt 1 beginnen müssen. 69 4 Betriebssystem Unix / Linux Oft werden auch binäre Semaphore eingesetzt. Sie werden mit 1 initialisiert und bewirken so den wechselseitigen Ausschluß von einer Ressource. Diesen wechselseitigen Ausschluß kann man auch mit Spinlocks lösen (siehe wechselseitiger Ausschluß mit Spinlocks). Bei der Verwendung von Semaphoren werden die Prozesse, falls ein kritischer Bereich belegt ist, angehalten. Sie warten im Zustand blockiert, bis der kritische Bereich frei ist, ohne das System zu belasten. Eine Semaphorgruppe wird vom System nicht automatisch gelöscht, wenn sie nicht mehr benötigt wird. Sie muß durch den entsprechenden semctl()Aufruf im Programmcode gelöscht werden. Von der Kommandooberfläche aus kann man sich die Semaphorgruppen mit dem Befehl ipcs -s anzeigen lassen und eventuell nicht mehr benötigte Semaphorgruppen mit dem Befehl ipcrm sem id (id der Semaphorgruppe) löschen. Schnittstellenfunktionen für die Arbeit mit Semaphoren: semctl Steuerfunktion für Semaphore Includes: #include <sys/ipc.h> #include <sys/sem.h> Prototyp: int semctl ( int semid, int semnum, int befehl, union semun arg ) ; Parameter: int semid int- Wert für die Identifikation der Semaphorgruppe. Rückgabewert von semget(). int semnum Nummer des Semaphors. int befehl Gibt an, was getan wird. SETALL Setzen aller Semaphorwerte GETALL Auslesen aller Semaphorwerte SETVAL Setzen eines bestimmten Semaphorwertes GETVAL Auslesen eines bestimmten Semaphorwertes IPC_RMID Löschen der Semaphorgruppe union semun arg union semun Parameter für befehl Wenn befehl== SETALL oder befehl== GETALL Ist arg ein Feld vom Typ unsigned short. In diesen Feldern stehen die Semaphorwerte, wie sie gesetzt werden sollen bzw. wie sie ausgelesen wurden. Bei befehl == SETVAL ist arg ein intWert für den bestimmten Semaphor. Bei befehl == GETVAL hat arg keine Bedeutung, da die Funktion den aktuellen Wert des einzelnen Semaphores als Rückgabewert zurückliefert. { int val; /* für SETVAL */ struct semid_ds buf; /* IPC_STAT /*/ /* und IPC_SET */ ushort *array } /*für GETALL u. SETALL*/ 70 4 Betriebssystem Unix / Linux Rückgabe: Bei allen GET-Befehlen außer GETALL liefert die Funktion den entsprechenden Wert. Bei anderen Befehlen ist der Rückgabewert gleich NULL oder –1 bei einem Fehler. Beispiel für die Funktion semctl(): #include <sys/ipc.h> #include <sys/sem.h> int semid; unsigned short unsigned short initarray[0] = initarray[1] = initarray[2] = initarray[3]; /* Initialisierungsfeld */ outarray[3]; /* Ausgabefeld */ 3; /* Wert des 1. Semaphores soll 3 sein */ 5; /* Wert des 2. Semaphores soll 5 sein */ 1; /* Wert des 3. Semaphores soll 1 sein */ semctl ( semid, 0, SETALL, initarray ); /* Initialisiert die Semaphore mit 3, 5 und 1 */ semctl ( semid, 0, SETVAL, 6 ); /* Initialisiert den 1. Semaphor mit 6 */ semctl ( semid, 0, GETALL, outarray ); /* Semaphorwerte werden in das Feld outarray geschrieben */ semctl ( semid, 0, IPC_RMID, 0 ); /* Löscht die Semaphorgruppe */ Achtung: Manche Unix bzw. Linux Versionen akzeptieren bei SETALL und GETALL keine Felder. Sie verlangen die Übergabe einer Union. Beispiel: union semun para ; union semun para2 ; unsigned short initarray[3]; unsigned short outarray[3]; initarray[0] = 3; initarray[1] = 5; initarray[2] = 1; para.array = initarray; /* Initialisierungsfeld */ /* Ausgabefeld */ /* siehe oben */ /* Zeigerzuweisung für */ /* Initialisierungsfeld */ para2.array = outarray; /* Zeigerzuweisung für Ausgabefeld*/ semctl ( semid, 0, SETALL, para ); semctl ( semid, 0, GETALL, para2 ); 71 4 Betriebssystem Unix / Linux semget Im System wird eine neue Semaphordatenstruktur angelegt. Bei einer bestehenden Gruppe wird der Zugriff ermöglicht. Includes: #include <sys/ipc.h> #include <sys/sem.h> Prototyp: int semget ( long schlüssel, int n, int modus ); Parameter: long schlüssel Schlüssel, der durch den Programmierer frei definierbar ist. Wird hier IPC_PRIVATE eingegeben und im Parameter modus IPC_CREAT so wird auf jeden Fall eine neue Semaphorengruppe angelegt. int n Anzahl der Semaphore in der Gruppe. int modus Bitmuster: Die letzten 9 Bitstellen geben die Zugriffsrechte auf die Semaphorgruppe an. Rückgabe: -1 bei einem Fehler. int-Wert für die Identifikation der Semaphorgruppe. Beispiele: #include <sys/ipc.h> #include <sys/sem.h> int semid; semid = semget ( IPC_PRIVATE, 1, IPC_CREAT|0777 ) ; /* Erzeugt Semaphorgruppe mit einem Semaphor mit allen Zugriffsrechten. Der interne Identifikationswert wird an semid übergeben.*/ oder semid = semget ( 20, 2, IPC_CREAT|0777 ) ; /* Erzeugt unter dem externen Schlüssel “20“ eine Semaphorgruppe mit zwei Semaphoren mit allen Zugriffsrechten. Der interne Identivikationswert wird an semid übergeben. */ oder semid = semget ( 20, 2, 0777 ) ; /*Ermittelt die unter dem externen Schlüssel “20“ erzeugte Semaphorgruppe . Der interne Identifikationswert wird an semid übergeben. */ 72 4 Betriebssystem Unix / Linux semop Führt die in einem Array gespeicherten Semaphorbefehle in einer atomaren Operation aus. Prototyp: int semop ( int semid, struct sembuf semoparray[], int anzahl ) ; Parameter: int semid int- Wert für die Identifikation der Semaphorgruppe. Rückgabewert von semget(). struct sembuf semoparray[] Zeiger auf ein Array mit Semaphorenoperationen. struct sembuf { ushort sem_num; short sem_op; short sem_flg; } /*Nummer des */ /* Elementes */ /* Operation die */ /* ausgeführt wird */ /* negativ, 0 oder */ /* positiv */ /* Legt fest was zu*/ /* tun ist falls */ /* die Operation */ /* nicht ausführbar*/ /* ist */ 4.7.2 Synchronisation durch Signale Bei der Synchronisation / Steuerung von Prozessen durch Signale kann man die Reihenfolge festlegen, in der bestimmte Prozesse bearbeitet werden. Die Funktionen signal(), pause() und kill() werden hierfür verwendet. Durch die Funktion signal() wird ein Signal-Handler, der beim Eintreffen des Signals ausgeführt wird, an das Signal gebunden. Bei größeren Programmen, die mehrere Prozesse haben, wird es allerdings schwierig den Überblick zu behalten. Bei dieser Methode wird die Reihenfolge festgelegt in der Prozesse bzw. Teile von Prozessen ausgeführt werden. Die “parallele“ Bearbeitung von Prozessen wird dadurch eingeschränkt. Ein weiteres Problem bei der Arbeit mit Signalen ist, daß Signale nicht vom System gespeichert werden. Erhält ein Prozess ein Signal bevor dieser selbst die Funktion pause() aufgerufen hat geht dieses Signal verloren und der Prozess wartet, wenn er später die Funktion pause() aufruft, vergeblich auf ein Signal. Schnittstellenfunktionen für die Arbeit mit Signalen: pause(), kill(), signal() siehe Kapitel 4.5 Erzeugung und Steuerung von Prozessen. 73 4 Betriebssystem Unix / Linux Beispiel für die Synchronisation durch Signale: #include<stdio.h> #include<signal.h> void sighand() /* Signal Handler wird beim Eintreffen */ { /* des Signales SIGUSR1 ausgefuehrt */ signal(SIGUSR1,&sighand); /* Hier wird die Bindung des Signals */ ** Programmcode der beim Eintreffen des Signals ausgeführt wird** } /* SIGUSR1 an den Signal Handler sighand()*/ /* erneuert. Dies muß nach jedem Eingang des /* Signals geschehen. */ main() { int vater_pid,prozess1_pid,prozess2_pid; */ /* PID‘s der Söhne */ signal (SIGUSR1,&sighand); /* Bindung des Signals SIGUSR1 */ /* an den Signal Handler sighand() */ if((prozess1_pid = fork())==0) /* Sohnprozess 1 wird erzeugt */ { /* und gestartet */ vater_pid=getppid(); /* Sohnprozess erfragt die */ for(hilf=0;hilf<=10;hilf++) /* PID des Vaters */ { ** Programmcode des 1. Sohnes wird durchlaufen ** } kill(vater_pid,SIGUSR1); /* Dem Vaterprozess wird das */ /* Signal SIGUSR1 gesendet */ exit(0); /* 1. Sohnprozess terminiert */ } if((prozess2_pid = fork())==0) /* Sohnprozess 2 wird */ { /* erzeugt und gestartet */ pause(); /* Sohnprozess 2 wartet auf ein Signal */ for(hilf=0;hilf<=10;hilf++) { ** Programmcode des 2. Sohnes wird durchlaufen ** } exit(0); /* 2. Sohnprozess terminiert */ } pause(); /* Vaterprozess wartet auf ein Signal */ kill(prozess2_pid,SIGUSR1); /* Vaterprozess sendet Signal */ return 0; /* an 2. Sohnprozess */ } 74 4 Betriebssystem Unix / Linux 4.7.3 Wechselseitiger Ausschluß mit Spinlocks Der Begriff „Spinlock“ ist sehr treffend (engl. Spin = drehen, kreisen). Da bei dieser Art des wechselseitigen Ausschlußes eine while-Schleife der Hauptbestandteil ist. Diese wird sooft durchlaufen, bis der Prozess Zugriff auf die benötigte Ressource erhält. Man unterscheidet zwei Arten. 1. Spinlocks durch Maschinenbefehle Hier wird durch Aufruf einer Funktion innerhalb einer while-Schleife ( while(TEST_AND_SET(&lock)); ) getestet, ob ein kritischer Bereich frei ist oder nicht. Dies wird anhand einer Sperrvariablen festgestellt. Wenn der kritische Bereich besetzt ist, liefert die Funktion z.B. den Wert TRUE zurück, sodaß die while-Schleife erneut durchlaufen wird. Ist der kritische Bereich frei, so erhält man den Wert FALSE und die Sperrvariable wird auf TRUE gesetzt. Hierbei ist wichtig, daß die Überprüfung und das Setzen der Variable ohne Unterbrechung also "atomar" erfolgt. Beim Verlassen des kritischen Bereiches setzt der Prozess die Sperrvariable einfach auf FALSE ( lock = FALSE ). 2. Spinlocks durch Lock Files Bei dieser Art von Spinlock wird anstelle einer Sperrvariable ein sogenannter Lock File verwendet. Dieser Lock File ist eine Datei mit einem bestimmten Namen. Dieser Name muß allen Prozessen bekannt sein die sich einen kritischen Bereich teilen. Ein Prozess versucht vor dem Eintritt in einen kritischen Bereich diese Datei anzulegen. Existiert diese Datei, ist der kritische Bereich bereits von einem anderen Prozess belegt. Verläßt dieser den kritischen Bereich, löscht er die Datei, die er beim Eintritt in diesen Bereich angelegt hat. Durch die while-Schleife versuchen die Prozesse solange diese Datei anzulegen, bis es ihnen gelingt. Das Erzeugen diese Lock Files muß “atomar“ erfolgen. Die Verwendung von Spinlocks zum gegenseitigem Ausschluß hat aber einige Nachteile. Es wird keine Reihenfolge festgelegt, so das es passieren kann, das einige Prozesse sehr lange warten müssen. Nach Freigabe des kritischen Bereiches besitzt jeder Prozess, auch der den kritischen Bereich gerade verlassen hat, die selbe Wahrscheinlichkeit den kritischen Bereich als nächstes zu erhalten. Ein weiterer und auch größerer Nachteil ist, daß die Prozesse nicht in den Zustand blockiert übergehen sondern immer wieder die while-Schleife durchlaufen, was zu einer Belastung des Systems beiträgt, da sie die CPU belasten. 75 4 Betriebssystem Unix / Linux 4.8 Prozesskommunikation Prozesse müssen die Möglichkeit haben Informationen untereinander auszutauschen obwohl sie unabhängig voneinander sind. Diese Kommunikation kann in Unix / Linux mit verschiedenen Methoden erreicht werden. Da es Pipes nicht nur in Unix / Linux gibt sondern auch in Windows NT und Java wird in dieser Diplomarbeit der Schwerpunkt auf diese Art der Kommunikation gelegt. 4.8.1 Pipes Pipe Schreibprozess Bytestrom 000101100110110 Schreibende Leseprozess Leseende Abb. 4.3: Pipe in Unix / Linux [Vogt 2001] Eine Pipe kann man sich als einen röhrenartigen Datenkanal vorstellen. Ein Prozess schreibt die Daten in diese Pipe und ein anderer Prozess kann diese Daten in der Reihenfolge auslesen, in der sie vom anderen Prozess geschrieben wurden. Eine Pipe in Unix / Linux ist unidirektional, so daß die Daten nur in eine Richtung übermittelt werden. Eine Pipe hat für einen Prozess das Aussehen einer Datei, auf die er schreibt oder liest. Außer dem Positionieren kann darauf jede Dateioperation erfolgen. Beim Lesen und Schreiben muß allerdings auf die Größe des Pipe-Buffers geachtet werden. Dieser PipeBuffer ist abbhänig vom System ( meistens 4kB oder 8kB groß ). Prozesse, die mit einer Pipe arbeiten werden in bestimmten Situationen vom System gesteuert. Ein Prozess, der aus einer leeren Pipe lesen will muß warten, bis von einem anderen Prozess in die Pipe geschrieben wurde. Ein Prozess, der in eine Pipe schreiben will muß warten, wenn der Pipe-Buffer voll ist. In Unix / Linux gibt es zwei Arten von Pipes, unbenannte und benannte. Die unbenannte Pipe (manchmal auch einfache Pipe genannt) hat einige Einschränkungen. Die Lebensdauer einer unbenannten Pipe ist abhängig von der Lebensdauer der Prozesse die mit ihr arbeiten. Sind alle Prozesse beendet, die mit der Pipe arbeiten, so wird die Pipe gelöscht. Es gibt meistens einen schreibenden und einen lesenden Prozess. 76 4 Betriebssystem Unix / Linux Die Kommunikation über eine unbenannte Pipe ist nur für Prozesse möglich, die mit einander “verwandt“ sind. Dies gilt für Prozesse, die eine Vater Sohn Beziehung zueinander besitzen, für Sohnprozesse die den selben Vater haben und für Enkelprozesse. In allen Fällen richtet der Vaterprozess die Pipe ein. Mit einem pipe() Aufruf besitzt ein Prozess eine Pipe zu sich selbst, aus der er mit fd[0] Daten lesen kann. Mit fd[1] kann er Daten in diese Pipe schreiben. Prozess A fd[1] fd[0] Abb. 4.4: Vaterprozess (Prozess A) richte Pipe ein [Herold1994] Diese Pipe erhält dann einen Sinn, wenn der Vaterprozess durch einen fork() Aufruf einen Sohnprozess kreiert, der mit dem Vaterprozess Daten austauscht. Dieser Sohnprozess erbt die Pipe seines Vaters. Die Abbildung 4.5 zeigt die Verwendung einer Pipe zwischen Vater und Sohn. Der Sohnprozess (Prozess B1) sendet Daten an den Vaterprozess. Die Richtung des Datenstromes wird dadurch beeinflußt welcher Prozess die Lese-bzw. Schreibseite der Pipe schließt. Mit dem Aufruf close(fd[0]) wird vom Sohnprozess die Leseseite der Pipe geschlossen. Der Vaterprozess schließt die Schreibseite der Pipe mit dem Aufruf close(fd[1]). (Vater) Prozess A fork() fd[0] Schreiber close (fd[1]) Prozess B1 fd[1] close (fd[0]) Abb. 4.5: Herstellen einer Pipe zwischen Vater und Sohn [Herold1994] 77 4 Betriebssystem Unix / Linux Sollen zwei Söhne durch eine unbenannte Pipe miteinander kommunizieren wie es die Abbildung 4.6 zeigt, so müssen folgende Schritte ausgeführt werden. 1. 2. 3. 4. 5. 6. 7. Vaterprozess richtet durch den Aufruf pipe() eine Pipe ein. Der Vaterprozess kreiert durch fork() einen “Schreib-Sohn“. Der Vaterprozess schließt durch close(fd[1]) die Schreibseite der Pipe. Der “Schreib-Sohn“ schließt die Leseseite durch close(fd[0]). Der Vaterprozess kreiert nun durch fork() einen “Lese-Sohn“. Der Vaterprozess schließt durch close(fd[0]) nun auch die Leseseite der Pipe. Dieser “Lese-Sohn“ schließt durch close(fd[1]) die Schreibseite der Pipe. (Vater) Prozess A fork() close (fd[0]) fork() Leser Schreiber Prozess B1 fd[1] Prozess B2 fd[0] close (fd[1]) Abb. 4.6: Herstellen einer Pipe-Verbindung zwischen “Schreib-Sohn” und “Lese-Sohn” [Herold1994] Die so erstellte Pipe bildet nun eine Kommunikationsverbindung zwischen dem 1. Sohn (Schreibprozess) und dem 2. Sohn (Leseprozess). Der Vaterprozess hat nach dem Erstellen keinen Einfluß auf die Pipe, da er die Lese-und Schreibseite geschlossen hat. Eine benannte Pipe ist eine Erweiterung gegenüber einer unbenannten Pipe. Sie besitzt einen angelegten Geräteeintrag vom Typ FIFO (First In First Out) und hat einen entsprechenden Namen, mit dem sie von jedem Prozess durch open() angesprochen werden kann. Dieser Name wird beim Aufruf des ls –l Kommandos angezeigt und durch ein p als Typenangabe gekennzeichnet. Eine benannte Pipe wird vom System nicht automatisch gelöscht, wenn alle Prozesse beendet sind. Durch den Aufruf unlink() muß der Anwender die benannte Pipe innerhalb eines Prozesses selber löschen. Eine Löschung der benannten Pipe ist auch von der Kommandooberfläche durch den Befehl rm möglich. 78 4 Betriebssystem Unix / Linux Schnittstellenfunktionen für die Arbeit mit Pipes: close Schließt ein Schreib-oder Leseende einer Pipe Prototyp: int close ( int fd ); Parameter: int fd Beispiel: siehe pipe() Aufruf mkfifo Erzeugt eine benannte Pipe. Prototyp: int mkfifo ( char *name, int mode ); Parameter: char name Name bzw. Pfad der Pipe. int mode Bitmuster für Zugriffsrechte auf die Pipe. Die Positon und Bedeutung dieser Bits sind so wie bei der Ausgabe des ls –l Kommandos. Lese-bzw. Schreibdeskriptor einer Pipe Rückgabe: 0 bei erfolgreicher Ausführung, ansonsten –1. Beispiel: mkfifo (“MY_PIPE“, 0777); /* Erzeugt eine benannte Pipe mit dem Namen MY_PIPE im selben Verzeichnis, in dem der Prozess gestartet wurde. 0777 ist der Oktalwert 777 das dem Bitmuster 111111111 entspricht. Damit haben allen Benutzer sämtliche Zugriffsrecht. */ open Öffnet eine Pipe bzw. Datei. Prototyp: int open ( char *name, int flag, int mode ); Parameter: char name Name bzw. Pfad der Pipe int flag Bitmuster für Zugriff auf die Pipe. O_RDONLY Lesezugriff O_WRONLY Schreibzugriff O_NONBLOCK gibt an, wie sich der Prozess verhalten soll. Wird O_NONBLOCK nicht angegeben (Normalfall), wird ein Leseprozess blockiert, bis ein anderer Prozess die Pipe zum Schreiben öffnet und umgekehrt. Rückgabe: -1 bei Fehler oder Dateideskriptor für Pipe bzw. Dateizugriff Beispiel: fd = open („MY_PIPE“, O_WRONLY) /* Öffnet die Pipe MY_PIPE zum Schreiben */ 79 4 Betriebssystem Unix / Linux pipe Erzeugt eine unbenannte Pipe. Prototyp: int pipe ( int fd[2] ); Parameter: int fd[2] Beispiel: main() { int fd[2]; char outbuf[6]; pipe(fd); /* Pipe wird erzeugt */ ** Sohnprozess erzeugen ** close (fd[0]); /* Sohnprozess schließt */ /* Leseende der Pipe */ write (fd[1], “Hallo“, 6); /* Sohnprozess schreibt */ /* in die Pipe*/ * Sohnprozess führt weiteren Code aus und terminiert * close(fd[1]); /* Vaterprozess schließt */ /* Schreibende der Pipe */ read (fd[0],outbuf,6); /* Vater liest Pipe aus */ ** Vaterprozess terminiert ** } read Auslesen der Daten aus einer Pipe. Ist die Pipe leer, blockiert die Funktion. Prototyp: int read ( int fd, char *outbuf, unsigned bytes ); Parameter: int fd Diskriptor der Pipe. char *outbuf Zeiger auf den Speicherbereich, indem die Daten gespeichert werden. unsigned bytes Maximale Anzahl der Bytes, die gelesen werden. Dateideskriptoren, die zurückgegeben werden. fd[0] Dateideskriptor für das Leseende der Pipe. fd[1] Dateideskriptor für das Schreibende der Pipe. Rückgabe: Anzahl der gelesenen Bytes, -1 bei einem Fehler und 0, wenn die Pipe am Schreibende geschlossen wurde. Beispiel: fd = open (“MY_PIPE“,O_RDONLY); lese = read(fd, outb, 2); /* Liest max. 2 Bytes aus */ /* der Pipe “MY_PIPE“ */ unlink Löscht die benannte Pipe. Prototyp: int unlink ( char* name ); Parameter: char *name Beispiel: unlink(“MY_PIPE“); /* Die Pipe MY_PIPE wird gelöscht */ Name der Pipe. 80 4 Betriebssystem Unix / Linux write Schreibt Daten in eine Pipe. Ist der Pipe-Buffer voll, blockiert diese Funktion. Prototyp: int write ( int fd, char *outbuf, unsigned bytes ); Parameter: int fd Diskriptor der Pipe. char *outbuf Zeiger auf den Speicherbereich, von dem die Daten geschrieben werden. unsigned bytes Maximale Anzahl der Bytes, die geschrieben werden. Beispiel: fd = open (“MY_PIPE“, O_WRONLY);/*Schreibt “HALLO“ in*/ write (fd, “HALLO“, 6); /* die Pipe “MY_PIPE“ */ 4.8.2 Message Queues Abb. 4.7: Aufbau von Message Queues in Unix / Linux [Vogt 2001] Bei dieser Art der Kommunikation werden die Daten an Nachrichtenspeicher, sogenannte “Message Queues“, gesendet und können dort von anderen Prozesssen abgeholt werden. Dies wird durch ein Array verwaltet (Message-Queue-Tabelle), indem Daten über jede Message Queue stehen. Die Nachrichten bestehen aus einem Nachrichtenkopf (Message Header) und einem Nachrichtentext. Im Message Header sind Informationen, wie Typ, Größe der Nachricht und ein Zeiger auf den Speicherbereich, wo die Nachricht steht, enthalten. 81 4 Betriebssystem Unix / Linux 4.8.3 Shared Memory Abb 4.8: Grundprinzip von Shared Memorry [Vogt 2001] Bei diesem Prinzip benutzen die Prozesse einen gemeinsamen Speicherbereich auf den sie zugreifen können. Dieser Speicherbereich muß durch das Beriebssystem gekennzeichnet bzw. registriert werden. Erfolgt der Zugriff auf diesen Speicherbereich durch mehrere Prozesse, müssen diese synchronisiert werden. 4.8.4 Sockets Client-Prozess Server-Prozess Socket-Kopf Socket-Kopf Protokoll-Stack Gerätetreiber TCP TCP IP IP z.B. Ethernettreiber Protokoll-Stack Gerätetreiber z.B. Ethernettreiber Rechnernetz Abb. 4.9: Das Socket-Modell am Beispiel TCP/IP [GuOb1995] 82 4 Betriebssystem Unix / Linux Ein Socket kann als Datenpunkt zur Kommunikation zwischen Prozessen betrachtet werden. Sockets ermöglichen eine bidirektionale Kommunikation sowohl lokal als auch innerhalb eines Netzwerkes. Der vom Benutzer aus sichtbare Teil der Kommunikation besteht aus drei Teilen (siehe Abb. 4.9): • dem Socket-Kopf (Socket Layer), • dem Protokollteil (Protocol Layer), • dem Gerätetreiber ( Device Layer). Der Socket-Kopf bildet die Schnittstelle zwischen den Betriebssystemaufrufen und den weiter unten liegenden Schichten. Welche Kombinationen von Sockets, Protokoll und Treiber, möglich sind, wird bei der Systemgenerierung festgelegt. Sockets mit gleicher Charakteristika, bezüglich Adressierung und des Protokolladreßformates, werden zu Bereichen, sogenannten Domains, zusammengefaßt. Die Unix System Domain dient dabei zur lokalen Kommunikation zwischen Prozessen. Die Internet Domain dient zur Kommunikation über ein Netzwerk. Client-Prozess Server-Prozess listen Socket accept Socket Abb. 4.10: Client- Server -Kommunikation [GuOb1995] Eine Kommunikation läuft in der Regel so ab, daß ein Server-Pozess einen Kommunikationspunkt (Socket) aufbaut. Ein Client-Prozess koppelt sich ebenfalls an einen (lokalen) Kommunikationspunkt (Socket) und beantragt einen Verbindungsaufbau zu dem Socket des Server-Prozesses. Der Server-Prozess macht mit dem Aufruf listen() dem System bekannt, daß er Verbindungen akzeptieren will und gibt die Länge einer Warteschlange an. Der Aufruf accept() erfolgt, wenn ein Client-Prozess eine Verbindung anfordert. Der accept()-Aufruf liefert nach einem Verbindungsaufbau dem Server-Prozess einen neuen Socket-Deskriptor (analog zu einem Dateideskriptor) für einen anderen Socket zurück, über den nun die Kommunikation mit dem Client-Prozess erfolgen kann. Wie in Abb. 4.10 zu sehen ist, ist der Socket, an dem der Server-Prozess auf Verbindung wartet, und der Socket, über den nach einem Verbindungsaufbau die Kommunikation stattfindet, auf der Serverseite nicht identisch. 83 4 Betriebssystem Unix / Linux 4.8.5 Streams Benutzerprozess Benutzeradreßraum Streams-Kopf Systemadreßraum Modul (optional) Treiber Abb. 4.11: Schemabild eines Streams [GuOb1995] Ein Stream ist ein Pseudotreiber im Betriebssystemkern, wobei der Begriff Pseudo hierbei verwendet wird, weil zunächst hinter dem Treiber kein physikalisches Gerät steht, sondern nur eine Reihe von Softwarefunktionen. Der Treiber stellt dabei eine Schnittstelle zwischen Benutzerprogramm und Beriebssystem zur Verfügung. Über diese Schnittstelle können Daten(ströme) in beide Richtungen und volldublex ausgetauscht werden. Ein Datenweg, der mit einem Stream-Mechanismus aufgebaut wurde besteht aus folgenden Komponenten (siehe Abb. 4.11): • dem Stream-Kopf (Stream Head), • einem oder mehrere optionalen Verarbeitungsmodulen, • einem an dem Stream angekoppelten Treiber. Der Treiber kann dabei ein Gerätetreiber für ein physikalisches Gerät oder ein Pseudotreiber sein. Eine mögliche Funktion eines Verarbeitungsmoduls kann z.B. in einem Netzwerk die Abarbeitung eines Netzwerkprotokolls sein. Eine wesentliche Eigenschaft des Streams-Mechanismus ist der, daß Verarbeitungsmodule dynamisch in den Verarbeitungsstrom eingeschaltet und wieder entfernt werden können. Wird ein neues Verarbeitungsmodul eingefügt, geschied dieses unmittelbar hinter dem Kopfmodul. Bereits vorhandene Verarbeitungsmodule werden dadurch nach "unten" verschoben. 84 5 Beispiel Applet 5 Beispiel Applet 5.1 Grundlegende Arbeitsweise eines Applets Applets sind Java-Programme, die auf einem Internet / Intranet-Server gespeichert sind. Sie werden auf verschiedene Client-Plattformen heruntergeladen und dort in einer Java Virtual Machine (JVM) ausgeführt, die von dem auf dem Client-Rechner ausgeführten Browser bereitgestellt wird. Um ein Applet in einer Webseite zu integrieren benötigt man ein sogenanntes <APPLET>Tag. In diesem <APPLET>-Tag stehen Informationen, die die JVM benötigt, um das Applet auszuführen. Bevor das Applet gestartet wird prüft das Klassenlademodul in der JVM welche Klassen benötigt werden. Beim Klassenladevorgang werden die Klassendateien überprüft um sicher zu sein, daß es sich um gültige Klassendateien und nicht um bösartigen Code handelt. Ist der Klassenladevorgang erfolgreich abgeschlossen wird das Applet ausgeführt. Benutzer, die ein Programm aus dem Internet herunterladen und ausführen, wollen sicher sein, daß dieses Programm keine bösartigen Aktionen ausführt wie z. B. das Formatieren der Festplatte oder das Öffnen von Verbindungen für "nicht vertrauenswürdige" Rechner. Deshalb erfolgt die Verteilung und Ausführung unter Aufsicht eines Security Managers, der Applets daran hindern kann, Aufgaben auszuführen. Da der Security Manager viele Aktivitäten, die bei einer "normalen" Anwendung selbstverständlich sind nicht zuläßt, muß der Entwickler eines Applets sich im Vorfeld Gedanken darüber machen, welche Einschränkungen der Security Manager verursacht. Standardmäßig werden alle Applets als "nicht vertrauenswürdig" betrachtet und von bestimmten Aktivitäten ausgeschlossen. Dazu zählen: • Lesen von und Schreiben auf die lokale Festplatte. Es ist nicht möglich, Dateien zu lesen, ihr Vorhandensein zu überprüfen, Dateien zu schreiben, Dateien umzubenennen usw. Dadurch wird verhindert, daß Informationen über den Benutzer erfaßt und gespeichert werden. • Verbinden mit einem anderen als dem Computer, von dem das Applet stammt. Dies erschwert die Bereitstellung von Daten aus einer Datenbank, die sich auf einem anderen Computer als der Webserver befindet. Es gibt weitere Aktivitäten (z. B. die Ausführung von Programmen auf lokalen Systemen, um Zeit zu sparen), die für Anwendungsentwickler selbstverständlich, in Applets jedoch nicht erlaubt sind. In jedem brauchbaren Java-Buch werden die Einschränkungen und eventuelle Lösungen aufgelistet, denen Applets unterliegen. [JBulider3] 85 5 Beispiel Applet 5.2 Eigenschaften des Beispiel Applets Abb. 5.1: Screenshot von der Benutzeroberfläche des Beispiel Applets Das Beispiel Applet in dieser Diplomarbeit soll jedem Anwender zeigen, wie sich die Synchronisation von Threads auswirkt. Zur besseren Übersicht wurde der Programmcode, den die einzelnen Threads ausführen einfach gehalten. Bei diesem Applet hat man die Möglichkeit drei Zähler und eine grafische Animation zu starten. Die drei Zähler des Applets und die grafische Animation sind unterschiedlich realisiert worden. Der 1. Zähler ( Counter genannt ) ist durch eine einfache Klasse mit dem Namen Counter implementiert worden. Diese Klasse wird nicht als Thread ausgeführt sondern als "normaler" Programmcode. 86 5 Beispiel Applet Der 2. Zähler ( Thread 1 genannt ) ist durch die Klasse CountingThread implementiert worden. Diese Klasse wurde jedoch von der Klasse Thread abgeleitet und die run() Methode mit dem Programmcode des Zählers überschrieben. Dieser Zähler wird daher als Thread ausgeführt. Der 3. Zähler ( Thread 2 genannt ) ist durch die Klasse CountingThreadRunnable implementiert worden. Diese Klasse besitzt das Interface Runnable und die run() Methode wurde mit dem Programmcode des Zählers überschrieben. Dieser Zähler wird daher als Thread ausgeführt. Die grafische Animation ( Thread 3 genannt ) ist durch die Klasse ImageCanvas implementiert worden. Diese Klasse wurde von der Klasse Canvas abgeleitet und besitzt das Interface Runnable und die run() Methode wurde mit dem Programmcode für die grafische Animation überschrieben. Die grafische Animation wird auch als Thread ausgeführt. Thread 3 soll eine Belastung des Systems verursachen, damit das Verhalten des Applets auch unter verschiedenen Bedingungen beobachtet werden kann. Die eventuelle Synchronisation findet, mit Ausnahme der "Synchronisation durch Priorität", zwischen Thread 1 und Thread 2 statt. Die Oberfläche des Applets kann in vier Hauptbereiche unterteilt werden : • Eingabefelder für die drei Zähler Über diese Felder kann eingestellt werden welchen Wertebereich die einzelnen Zähler durchlaufen und wie lange die Pause zwischen den einzelnen Zählerschritten sein soll. Bei den Eingabefelder für die Zähler, die als Thread ausgeführt werden kann zusätzlich die Priorität eingestellt werden, die sie beim Starten besitzen sollen. • Buttons zum Steuern der Threads Die Buttons unterhalb der Eingabefelder dienen zum Starten bzw. Beenden der Threads. Bei den Programmteilen, die als Thread ausgeführt werden kann zusätzlich die aktuelle Priorität geändert werden. • Ausgabefeld für die Zähler und Steuerinformationen Über das Ausgabefeld des Applets werden die Zählerstände der Zähler und Informationen über die Threads bzw. dem Counter ausgegeben. Da alle Ausgaben in einem Feld erfolgen hat man später eine genauere Übersicht in welcher Reihenfolge die Meldungen erfolgten. 87 5 Beispiel Applet • Synchronisationsfeld in der die Synchronisationsart umgestellt werden kann Im Synchronisationsfeld rechts neben dem Ausgabefeld kann eingestellt werden, ob und wenn ja, auf welche Weise synchronisiert werden soll. Wird die Einstellung geändert während die Threads laufen werden die beiden Zähler gestoppt. Folgende Arten der Synchronisation können ausgewählt werden: • ja, mit Ereignis Bei dieser Art wird durch ein Ereignis synchronisiert. Als Ereignis dient die Anzahl der Ausgaben, die ein Thread ohne Unterbrechung vornehmen kann. Diese Anzahl kann während die Threads laufen verändert werden. • ja, mit Priorität Soll eine Steuerung der Threads über die Priorität erfolgen wird die "sleep-Zeit" von Thread 1 und Thread 2 durch eine Anzahl von Schleifen ersetzt, die während der Ausführung der Threads geändert werden kann. Dies wird gemacht, da ein Thread während er schläft den Prozessor freigibt und so andere Threads, auch wenn sie eine niedrigere Priorität besitzen, ausgeführt werden. Man würde ohne die rechenintensiven Schleifen keinen Unterschied zwischen den einzelnen Threads, die eine unterschiedliche Priorität besitzen, bemerken. • ja, auf das Ende von Thread 2 Wie der Titel schon sagt, wird die Reihenfolge, in der die beiden Zähler Threads ausgeführt werden, beeinflußt. Erst wenn Thread 2 beendet ist darf Thread 1 seine Ausführung beginnen. Die Reihenfolge, in der die beiden Threads gestartet werden, spielt hierbei eine große Rolle. Bei der Auswahl dieser Synchronisationsart können die beiden Zähler Threads nur mit dem Button "Threads 1 u. Thread 2 starten" gestartet werden. • nein Wird "nein" ausgewählt findet keine Synchronisation statt. Zusätzlich zu diesen Bereichen findet man am unteren rechten Rand noch zwei Buttons. Der Button "Alle Threads anhalten" bzw. "Alle Threads weiterführen" dient dazu, alle laufenden Threads anzuhalten oder weiterzuführen, ohne das die Synchronisation unterbrochen wird. Jeder Thread arbeitet nach der Unterbrechung so weiter wie vorher. Der "RESET" Button versetzt die Threads und das Ausgabefeld wieder in den Startzustand. Führt man das Applet im Browser aus, wird die maximale Priorität der Threads vom System so begrenzt, daß wichtige Systemprozesse weiter arbeiten können und somit das System lauffähig bleibt (siehe Kapitel 2.6). Wird das Applet in einem Appletviewer ausgeführt besteht diese Begrenzung nicht. In so einem Fall kann die Priorität bis auf das Maximum (Prioritätswert 10) eingestellt werden. Hierbei ist darauf zu achten, daß die Lauffähigkeit des Systems bei der Vergabe von zu hohen Prioritäten eingeschränkt oder sogar gestoppt wird. 88 5 Beispiel Applet 5.3 Klassen des Applets Die Klassen, Konstruktoren und Methoden des Beispiel Applets werden in diesem Kapitel genauer erläutert. Im Source-Code befinden sich zusätzliche Erläuterungen, so daß nicht alle Attribute der Klassen hier aufgelistet werden. Auf eine zusätzliche Visualisierung durch UML (Unified Modeling Language) Diagramme, wie z.B. einem Klassendiagramm, wurde ebenfalls verzichtet. 5.3.1 Die Klasse Applet1.java Die Klasse Applet1 ist von der Klasse Applet abgeleitet und besitzt die Interfaces ActionListener und FocusListener. Diese Klasse erzeugt die Eingabemaske. Die Hauptkomponenten dieser Oberflächen sind die zahlreichen Eingabefelder (javax.swing.JTextField), Bezeichner für die Oberfläche (javax.swing.JLabel) und die Aktionsknöpfe (javax.swing.JButton). Diese Komponenten werden hier nicht eingehender erläutert, da sie im Source-Code kommentiert sind. Attribute: private boolean sync Variable für Synchronisation durch Ereignisse. private boolean syncjoin Variable zur Synchronisation auf das Ende eines Threads. private CountingThread thread1 Objekt für Thread 1. private Thread thread2, thread3 Objekte für Thread 2 und Thread 3. private Runnable rthread, bthread Runnable Objekte für die Klassen mit dem Interface Runnable. private int[] thindex = new int [3] Thread Index um festzuhalten welcher Thread gestartet wurde. 89 5 Beispiel Applet private String sleepthread1 private String sleepthread2 Variable um sleep-Zeit zu speichern, wenn durch Priorität synchronisiert wird. private Steuerung steuer = new Steuerung(false) Objekt um die Threads anzuhalten und wieder auszuführen. private Beenden stop2 = new Beenden(false) private Beenden stop3 = new Beenden(false) stop2 ist ein Objekt der Klasse Beenden und dient dazu dem Thread 2 anzuzeigen, das er seine run() Methode beenden soll. stop3 ist für Thread 3. private Syncprio syncprio Diese Objekt dient zur Synchronisation durch Priorität. private IntJTextField jTextFieldA[] = new IntJTextField[13] Feld für 13 Textfelder (Eingabefelder). MeineTextArea textArea1 Ausgabefeld für alle Threads und Zusatzinformationen. Methoden: public String getParameter(String key, String def) Parameterwert zum Starten des Applets holen. public Applet1() Das Applet konstruieren. public void init() Das Applet initialisieren. private void jbInit() Initialisierung der Komponenten. public String getAppletInfo() Applet-Informationen werden geholt. public String[][] getParameterInfo() Parameter-Infos holen. 90 5 Beispiel Applet public static void main(String[] args) Main-Methode public void actionPerformed(ActionEvent e) ActionPerformed, der ausgeführt wird sobald ein Button betätigt wird. void thread1start() Eigene Methode um Thread 1 zu erzeugen und ggf. zu starten. void thread2start() Eigene Methode um Thread 2 zu erzeugen und ggf. zu starten. void jButton1_actionPerformed(ActionEvent e) Methode, die durch Betätigung eines Buttons ausgeführt wird, um die Priorität von Thread 1 herabzusetzen. void jButton2_actionPerformed(ActionEvent e) Methode, die durch Betätigung eines Buttons ausgeführt wird, um die Priorität von Thread 1 heraufzusetzen. void jButton3_actionPerformed(ActionEvent e) Methode, die durch Betätigung eines Buttons ausgeführt wird, um die Priorität von Thread 2 herabzusetzen. void jButton4_actionPerformed(ActionEvent e) Methode, die durch Betätigung eines Buttons ausgeführt wird, um die Priorität von Thread 2 heraufzusetzen. void jButton5_actionPerformed(ActionEvent e) Methode, die durch Betätigung eines Buttons ausgeführt wird, um die Priorität von Thread 3 herabzusetzen. void jButton6_actionPerformed(ActionEvent e) Methode, die durch Betätigung eines Buttons ausgeführt wird, um die Priorität von Thread 3 heraufzusetzen. public void focusLost(FocusEvent e) Methode des FocusListener wird ausgeführt sobald ein Objekt den Focus verliert. 91 5 Beispiel Applet public void focusGained(FocusEvent e) Methode des FocusListener wird ausgeführt sobald ein Objekt den Focus erhält. public boolean test() Methode, die kontrolliert, ob alle Textfelder in ihrem Wertebereich sind. public void setsync() Diese Methode stellt fest ob die Ausgabe synchronisiert werden soll. void groupBox1_itemStateChanged(ItemEvent e) Methode wird ausgeführt, wenn durch Ereignisse synchronisiert werden soll. Sie setzt dann die entsprechenden Parameter. void groupBox2_itemStateChanged(ItemEvent e) Methode wir ausgeführt, wenn durch die Priorität synchronisiert werden soll. Sie setzt dann die entsprechenden Parameter. void groupBox3_itemStateChanged(ItemEvent e) Methode wird ausgeführt, wenn auf das Ende des zweiten Threads synchronisiert wird. Sie setzt dann die entsprechenden Parameter. void groupBox4_itemStateChanged(ItemEvent e) Methode wird ausgeführt, wenn nicht synchronisiert werden soll. Sie setzt dann die entsprechenden Parameter. void jTextFieldA11_actionPerformed(ActionEvent e) Methode wird ausgeführt, wenn eine Eingabe im Textfeld 11 erfolgte. Dieses Eingabefeld bestimmt die Anzahl der Ausgaben. void jTextFieldA12_actionPerformed(ActionEvent e) Methode wird ausgeführt, wenn eine Eingabe im Text Feld 12 erfolgte. Dieses Eingabefeld bestimmt die Anzahl der Schleifen. class Applet1_jTextFieldA11_actionAdapter implements java.awt.event.ActionListener Klasse des action_Adapter für das Eingabefeld 11 public void actionPerformed(ActionEvent e) Methode des action_Adapter für das Eingabefeld 11 92 5 Beispiel Applet class Applet1_jTextFieldA12_actionAdapter implements java.awt.event.ActionListener Klasse des action_Adapter für das Eingabefeld 12 public void actionPerformed(ActionEvent e) Methode des action_Adapter für das Eingabefeld 12 5.3.2 Die Klasse Beenden.java Diese Klasse dient dazu, die Thredas 2 und 3, die mit Hilfe des Interfaces Runnable erstellt wurden, zu beenden. Durch ein Objekt dieser Klasse können die beiden Threads "erfragen" ob sie sich beenden sollen. Konstruktor: public Beenden(boolean wert) Beim Erzeugen des Objekts muß der Anfangswert übergeben werden. Attribut: private boolean stop Speichert den Wert des Objektes. Methoden: public void set(boolean wert) Methode um den Wert des Objektes zu ändern. public boolean get() Methode um den Wert des Objektes abzufragen. 93 5 Beispiel Applet 5.3.3 Die Klasse Counter.java Diese Klasse ist ein Zähler, der einen übergebenen Wertebereich durchläuft. Konstruktor: public Counter(MeineTextArea textArea1) Beim Erzeugen wird die Text Area übergeben, auf die die Ausgabe erfolgen soll. Attribute: private int start Startwert des Zählers. private int finish Ende des Zählers. private long time Zeit, die der Zähler zwischen den Zählerschritten "schlafen" soll. MeineTextArea textArea1 TextArea für die Ausgabe. Methode: public void setcounter (int from, int to, long zeit) Daten für den Zähler werden übernommen und der Zähler wird ausgeführt. 5.3.4 Die Klasse CountingThread.java Diese Klasse ist von Thread abgeleitet und beinhaltet einen Zähler, der somit als Thread ausgeführt wird. Konstruktor: public CountingThread(Syncprio syncprio,MeineTextArea textArea1, Steuerung steuer,JLabel jLabel12) Beim Erzeugen werden Objekte von anderen Klassen übergeben. Diese Objekte werden zur Steuerung und Ausgabe des Threads benötigt. 94 5 Beispiel Applet Attribute: private int start private int finish private long time Variablen wie bei der Klasse Counter private boolean stop Variable für eigene "stop"-Methode des Threads MeineTextArea textArea1 TextArea für Ausgabe. Steuerung steuer Steuerobjekt zum Überprüfen, ob der Thread anhalten soll. Syncprio syncprio Steuerobjekt für Synchronisation durch Priorität. JLabel jLabel12 Ausgabefeld der Priorität Methoden: public void meinstop() Modifizierte "stop"-Methode. public void setcountingthread(int from, int to,long zeit) Methode um die Daten für den Zähler zu übernehmen. public void joinset(Thread thread) Modifizierte "start"-Methode. Sie dient zum Starten, wenn auf das Ende eines andern Thread gewartet werden soll. public void run() run() Methode des Threads. 95 5 Beispiel Applet 5.3.5 Die Klasse CountingThreadRunnable.java Diese Klasse beinhaltet ebenfalls ein Zähler. Sie besitzt das Interface Runnable wodurch der Programmcode ebenfalls als Thread ausgeführt wird. Konstruktor: public CountingThreadRunnable(JLabel jLabel13,Syncprio syncprio,Beenden stop2,Steuerung steuer, MeineTextArea textArea1,int from, int to,long zeit) Beim Erzeugen werden Objekte von anderen Klassen und die Daten für den Zähler übernommen. Die Objekte dienen der Steuerung und der Ausgabe des Zählers. Attribute: private int start private int finish private long time Variablen wie bei der Klasse Counter Beenden stop Objekt zum Beenden des Threads MeineTextArea textArea1 TextArea für Ausgabe. Steuerung steuer Steuerobjekt zum Überprüfen, ob der Thread anhalten soll. Syncprio syncprio Steuerobjekt für Synchronisation durch Priorität. JLabel jLabel13 Ausgabefeld der Priorität. Methode: public void run() run() Methode des Interfaces Runnable. 96 5 Beispiel Applet 5.3.6 Die Klasse ImageCanvas.java Diese Klasse ist abgeleitet von der Klasse Canvas und besitzt das Interface Runnable. Sie wird für die grafische Animation benötigt, die ebenfalls als Thread ausgeführt wird. Konstruktor: public ImageCanvas(Image images[],Steuerung steuer,Beenden stop3, Syncprio syncprio) Es wird ein Feld, das die Bilder enthält und Objekte anderer Klassen, übergeben. Attribute: private Image images[] Feld für die Bilder, die ausgegeben werden sollen. private int currentimage Variable für Bild. Beenden stop Objekt zum Beenden des Threads Steuerung steuer Steuerobjekt zum Überprüfen, ob der Thread anhalten soll. Syncprio syncprio Steuerobjekt für Synchronisation durch Priorität. Methode: public void paint(Graphics g) Methode zum Zeichnen des Bildes. public void update(Graphics g) Methode zum Zeichnen des Bildes. public void run() run() Methode des Interfaces Runnable. 97 5 Beispiel Applet 5.3.7 Die Klasse IntJTextField.java Diese Klasse ist abgeleitet von der Klasse JTextField. Einige Methoden wurden ergänzt bzw. der Konstruktor so verändert, daß die Objekte Eingabefelder mit einem festen Wertebereich sind. Konstruktor: public IntJTextField (int min, int max) Konstruktor der Klasse dem beim Erzeugen des Objektes der erlaubte Wertebereich der Eingabe übergeben wird. Attribute: private int low Untere Grenze des Wertebereiches. private int high Obere Grenze des Wertebereiches Methoden: public boolean isValid() Methode um festzustellen, ob die Eingabe des Objektes ein Int-Wert im erlaubten Wertebereich ist. public int getValue() Methode zum Auslesen des Textfeldes. 98 5 Beispiel Applet 5.3.8 Die Klasse MeineTextArea.java Die Klasse ist von der Klasse TextArea abgeleitet und dient zur synchronisierten und nicht synchronisierten Ausgabe der Threads. Konstruktor: public MeineTextArea() Parameterloser Konstruktor Attribute: private String buffer Buffer für die Ausgabe. private int soll Anzahl der "soll" Ausgaben. private int anzahl Anzahl der "ist" Ausgaben boolean th1 Variable die bei einer Synchronisation durch Ereignisse angibt welcher Thread den Monitor erhält. boolean sync Variable die angibt ob eine Synchronisation durch Ereignisse stattfinden soll. 99 5 Beispiel Applet Methoden: public void set(boolean syncronisieren,int anzahl) Methode zum Starten und Beenden der Synchronisation durch Ereignisse. public synchronized void put1 (String ausgabe) Synchronisierte Methode für die Ausgabe von Thread 1 public synchronized void put2 (String ausgabe) Synchronisierte Methode für die Ausgabe von Thread 2 public synchronized void put (String ausgabe) Synchronisierte Methode für die Ausgabe von allgemeinen Informationen. 5.3.9 Die Klasse Steuerung.java Über ein Objekt dieser Klasse können die Threads angehalten und zu einem beliebigen Zeitpunkt weitergeführt werden. Konstruktor: public Steuerung(boolean wert) Beim Erzeugen des Objektes muß der Anfangswert übergeben werden. Attribute: private boolean halt Diese Variable zeigt an ob die Threads anhalten sollen oder nicht. Methoden: public void setanhalten () Methode um der Wert des Objektes auf true zu setzen. public synchronized void weiter() Methode um den Wert des Objektes auf false zu setzen. public synchronized void anhalten () Methode bei dessen Ausführung ein Thread angehalten wird, wenn das "Steuerugsobjekt" true ist. 100 5 Beispiel Applet 5.3.10 Die Klasse Syncprio.java Ob eine Synchronisation durch die Priorität stattfindet, wird durch ein Objekt dieser Klasse angezeigt. Die benötigten Schleifen um das System zu belasten gehören auch zu dieser Klasse. Konstruktor: public Syncprio(boolean wert, int schleifen) Die Schleifenzahl und ob eine Synchronisation durch die Priorität erfolgen soll wird beim Erzeugen festgelegt. Attribute: private boolean sync Zeigt an ob synchronisiert werden soll private int schleifen Gibt an wie oft die Threads die Methode "zeit()" aufrufen sollen. Methoden: public void setschleifen(int schleifen) Methode mit der die Schleifenzahl verändert werden kann. public int getschleifen() Methode über die die Schleifenzahl erfragt werden kann. public void setsync(boolean wert) Methode über die festgelegt wird ob eine Synchronisation durch die Priorität erfolgen soll oder nicht. public boolean getsync() Methode über die erfragt werden kann ob eine Synchronisation durch die Priorität erfolgen soll. public synchronized int zeit() Methode um bei einer Synchronisation durch die Priorität das System zu belasten. 101 6 Beispielprogramme 6 Beispielprogramme Um die Beispielprogramme möglichst verständlich zu halten wurde in den meisten Fällen ein Zähler verwendet, der seinen jeweiligen Zählerstand auf dem Bildschirm ausgibt bzw. seinen Zählerstand einem anderen Prozess / Thread mitteilt. Um eine Systembelastung zu erreichen wurden Schleifen in diese Zähler integriert, die zusätzlich durchlaufen werden. Dies hat zur Folge, daß die Geschwindigkeit der Programme stark von dem jeweiligem Rechner abhängig ist auf dem sie ausgeführt werden. Sollte ein Programm zu schnell oder zu langsam auf dem jeweiligen Rechner sein, muß einfach die Anzahl der Schleifendurchläufe anpaßt werden. Die Beispielprogramme zu dieser Diplomarbeit wurden mit verschiedenen Werkzeugen erstellt die im Anhang genauer erläutert werden. 102 6 Beispielprogramme 6.1 Beispielprogramme für Java 6.1.1 Counter Das Programm Counter ist ein Zähler, der die Zahlen 0 bis 10 auf dem Bildschirm ausgibt. Source-Code Counter (Dateiname: Counter.java): import java.io.*; public class Counter { public static void main(String[] args) { int read; byte buffer[] = new byte[20]; System.out.println("Zum Starten und Beenden bitte Eingabetaste druecken"); try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; // Wartet auf das Druecken der Eingabetaste int lauf,lauf2,hilf; for(hilf=0;hilf<=10;hilf++) // Schleife fuer einen Zaehler { // Ausgabe des Zaehlers auf dem System.out.println("Counter : "+hilf); // Bildschirm for(lauf=0;lauf<=10000;lauf++) for(lauf2=0;lauf2<=10000;lauf2++); // Schleife um das // System zu belasten } try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; } // Wartet auf das Druecken der Eingabetaste } Bildschirmausgabe Counter: 103 6 Beispielprogramme 6.1.2 Thread Das Programm Thread ist, wie das Programm Counter, ein Zähler. Der Unterschied zwischen den beiden Programmen liegt darin, daß bei dem Programm Thread der Programmcode für den Zähler als Thread programmiert wurde. Dadurch kann das Programm Thread vom Anwender unterbrochen werden. Source-Code Thread (Dateiname EinThread.java): import java.io.*; class CountingThread extends Thread { // Klasse wird von Thread abgeleitet CountingThread(){}; // Konstruktor der Klasse public void run(){ // Programmcode der als Thread int lauf,lauf2,hilf; // abgearbeitet werden soll for(hilf=0;hilf<=10;hilf++) // Schleife fuer einen Zaehler { System.out.println("CountingThread : "+hilf); for(lauf=0;lauf<=10000;lauf++) // Schleife um das for(lauf2=0;lauf2<=10000;lauf2++); // System zu belasten } } } class EinThread { //Main-Methode public static void main(String[] args) { int read; byte buffer[] = new byte[20]; System.out.println("Zum Starten und Beenden bitte Eingabetaste druecken"); try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; System.out.println("Thread gestartet\n"); CountingThread thread1 = new CountingThread(); // Thread "thread1" wird thread1.start(); // Thread "thread1" wird try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; thread1.stop(); // Thread "thread1" wird } // wenn er nicht beendet erzeugt gestartet gestoppt, ist. } 104 6 Beispielprogramme Bildschirmausgabe Thread: 105 6 Beispielprogramme 6.1.3 Zwei Threads Bei diesem Programm wurden zwei Zähler als Threads programmiert. Beide Zähler laufen völlig unabhängig von einander. Source-Code Zwei Threads (Dateiname ZweiThreads.java): import java.io.*; class CountingThread extends Thread { CountingThread(){}; // Klasse wird von Thread abgeleitet public void run(){ int lauf,lauf2,hilf; // Programmcode der als Thread // abgearbeitet werden soll for(hilf=0;hilf<=10;hilf++) // Schleife fuer einen Zaehler { System.out.println("Thread 1 : "+hilf); for(lauf=0;lauf<=10000;lauf++) // Schleife um das System for(lauf2=0;lauf2<=10000;lauf2++); // zu belasten } } } class CountingThreadRunnable implements Runnable { CountingThreadRunnable(){}; // Klasse mit dem Interface Runnable public void run(){ int lauf,lauf2,hilf; // Programmcode der als Thread // abgearbeitet werden soll for(hilf=0;hilf<=10;hilf++) // Schleife fuer einen Zaehler { System.out.println(" Thread 2 : "+hilf); for(lauf=0;lauf<=10000;lauf++) for(lauf2=0;lauf2<=6000;lauf2++); // Schleife um das System } // zu belasten } } class ZweiThreads { //Main-Methode public static void main(String[] args) { int read; byte buffer[] = new byte[20]; System.out.println("Zum Starten und Beenden bitte Eingabetaste druecken"); try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; System.out.println("Threads gestartet\n"); 106 6 Beispielprogramme Runnable rthread = new CountingThreadRunnable(); // Runnable-Objekt wird erzeugt Thread thread2 = new Thread(rthread); // Thread "thread2" wird erzeugt // und das Runnable-Objekt uebergeben CountingThread thread1 = new CountingThread(); // Thread "thread1" wird erzeugt thread1.start(); // Beide Threads werden gestartet thread2.start(); System.out.println("Prioritaet Thread 1 : "+thread1.getPriority()); System.out.println("Prioritaet Thread 2 : "+thread2.getPriority()+"\n"); // Die Prioritaet der Threads wird // ermittelt und ausgegeben. try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; thread1.stop(); thread2.stop(); // Beide Threads werden gestoppt, } // wenn sie nicht beendet sind. } Bildschirmausgabe Zwei Threads: 107 6 Beispielprogramme 6.1.4 Priorität In diesem Programm werden zwei Zähler und eine Steuerung als Threads gestartet. Die Steuerungseinheit erhält die höchste Priorität. Beide Threads besitzen am Anfang die Standardpriorität (Prioritätswert = 5). Nach einigen Sekunden wird die Priorität der beiden Threads geändert. Die Priorität von Thread 1 wird auf 3 herabgesetzt und die Priorität von Thread 2 wird auf 7 erhöht. Source-Code Priorität (Dateiname ZweiThreadsPrio.java): import java.io.*; class CountingThread extends Thread { // Klasse wird von Thread abgeleitet CountingThread(){}; public void run(){ // Programmcode der als Thread int lauf,lauf2,hilf; // abgearbeitet werden soll for(hilf=0;hilf<=10;hilf++) { System.out.println("Thread 1 : "+hilf); for(lauf=0;lauf<=10000;lauf++) for(lauf2=0;lauf2<=10000;lauf2++); } } } class CountingThreadRunnable implements Runnable { // Klasse mit dem Interface Runnable. CountingThreadRunnable(){}; public void run(){ // Programmcode der als Thread int lauf,lauf2,hilf; // abgearbeitet werden soll for(hilf=0;hilf<=10;hilf++) { System.out.println(" Thread 2 : "+hilf); for(lauf=0;lauf<=10000;lauf++) for(lauf2=0;lauf2<=8000;lauf2++); } } } class Steuerung extends Thread { Thread thread1; // Klasse zur Steuerung der Threads Thread thread2; // wird von Thread abgeleitet Steuerung(Thread t1, Thread t2){// Threads, die gesteuert werden, thread1 = t1; // werden hier uebernommen. thread2 = t2; } 108 6 Beispielprogramme public void run() { System.out.println("Steuerung als Thread gestartet"); this.setPriority(Thread.MAX_PRIORITY); // Prioritaet des Steuerthreads wird // auf maximum gesetzt System.out.println("Prioritaet des Steuerungsthreads erhoeht auf "+this.getPriority()+"\n"); thread1.setPriority(Thread.NORM_PRIORITY); thread2.setPriority(Thread.NORM_PRIORITY); // Prioritaet der zu steuernden Threads wird // auf NORM_PRIORITY ( Prio = 5 ) gesetzt thread1.start(); // Threads werden gestartet thread2.start(); System.out.println("Threads zum Zaehlen gestartet\n"); System.out.println("Prioritaet Thread 1 : "+thread1.getPriority()); System.out.println("Prioritaet Thread 2 : "+thread2.getPriority()+"\n"); // Prioritaet der Threads wird ausgegeben try {sleep(3000);} // Steuerthread wartet 3 Sekunden catch(InterruptedException e){}; System.out.println("\nDie Prioritaet von beiden Threads wird geaendert"); thread1.setPriority(Thread.NORM_PRIORITY-2); thread2.setPriority(Thread.NORM_PRIORITY+2); // Prioritaet der Threads wird um 2 gesenkt bzw. erhoeht System.out.println("Prioritaet Thread 1 : "+thread1.getPriority()); System.out.println("Prioritaet Thread 2 : "+thread2.getPriority()+"\n"); } // Prioritaet der Threads wird ausgegeben } class ZweiThreadsPrio { // Main-Methode public static void main(String[] args) { int read; byte buffer[] = new byte[20]; System.out.println("Zum Starten und Beenden bitte Eingabetaste druecken"); try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; Runnable rthread = new CountingThreadRunnable(); // Runnable-Objekt wird erzeugt Thread thread2 = new Thread(rthread); // Thread "thread2" wird erzeugt // und das Runnable-Objekt uebergeben CountingThread thread1 = new CountingThread(); // Thread "thread1" wird erzeugt 109 6 Beispielprogramme Steuerung steuer = new Steuerung (thread1,thread2); // Steuerungsthread wird erzeugt und steuer.start(); // gestartet. Die zu steuernden Threads // werden beim Erzeugen uebergeben. try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; thread1.stop(); thread2.stop(); // Alle Threads werden gestoppt, steuer.stop(); // wenn sie nicht beendet sind. } } Bildschirmausgabe Priorität: 110 6 Beispielprogramme 6.1.5 Monitore Bei diesem Programm werden zwei Threads getartet. Beide Threads benutzen die selbe synchronisierte Methode eines Objektes. Obwohl diese Methode durch eine Schleife "verlangsamt" wurde entsteht keine Überschneidung der Ausgabe. Erst wenn ein Thread diese Methode vollständig beendet hat darf ein anderer Thread diese Methode für seine Ausgabe verwenden. Source-Code Monitore (Dateiname SyncMonitor.java): import java.io.*; class CountingThread extends Thread { Ausgabe ausg; // Klasse wird von Thread abgeleitet public CountingThread(Ausgabe ausg){ this.ausg=ausg; // Objekt zur Ausgabe wird uebernommen } public void run(){ int lauf,lauf2,hilf; // Programmcode der als Thread // abgearbeitet werden soll for(hilf=0;hilf<=3;hilf++) { ausg.put("Thread 1 : "+hilf,1); // Ausgabe des Zaehlers // ueber das Ausgabe-Objekt for(lauf=0;lauf<=10000;lauf++) for(lauf2=0;lauf2<=100;lauf2++); } } } class CountingThreadRunnable implements Runnable { Ausgabe ausg; // Klasse mit dem Interface Runnable public CountingThreadRunnable(Ausgabe ausg){ this.ausg=ausg; // Objekt zur Ausgabe wird uebernommen } public void run(){ int lauf,lauf2,hilf; for(hilf=0;hilf<=3;hilf++) { ausg.put(" Thread 2 : "+hilf,2); // Ausgabe des Zaehlers ueber das Ausgabe-Objekt for(lauf=0;lauf<=10000;lauf++) for(lauf2=0;lauf2<=60;lauf2++); } } } 111 6 Beispielprogramme class Ausgabe { // Klasse Ausgabe: Ueber ein Objekt dieser Klasse // soll die Ausgabe auf den Bildschirm erfolgen private String buffer; private int nummer; public Ausgabe(){ } public synchronized void put (String ausgabe,int nummer){ // Synchronisierte Methode ueber // die eine Ausgabe erfolgen soll. this.buffer=ausgabe; this.nummer=nummer; if(nummer==1) System.out.println("Thread Nummer "+nummer+" beginnt die synchronisierte Methode"); else System.out.println(" Thread Nummer "+nummer+" beginnt die synchronisierte Methode"); System.out.println(buffer); for(int lauf=0;lauf<=10000;lauf++) // Schleife um die Methode for(int lauf2=0;lauf2<=10000;lauf2++); // zu verlangsamen if(nummer==1) System.out.println("Thread Nummer "+nummer+" beendet die synchronisierte Methode\n"); else System.out.println(" Thread Nummer "+nummer+" beendet die synchronisierte Methode\n"); } } class SyncMonitor { // Main-Methode public static void main(String[] args) { int read; byte buffer[] = new byte[20]; Ausgabe ausg = new Ausgabe(); // Ausgabe-Objekt wird erzeugt System.out.println("Zum Starten und Beenden bitte Eingabetaste druecken"); try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; System.out.println("Threads gestartet\n"); Runnable rthread = new CountingThreadRunnable(ausg); Thread thread2 = new Thread(rthread); CountingThread thread1 = new CountingThread(ausg); // Threads werden erzeugt und das Ausgabe-Objekt uebergeben 112 6 Beispielprogramme thread1.start(); // Threads werden gestartet thread2.start(); System.out.println("Prioritaet Thread 1 : "+thread1.getPriority()); System.out.println("Prioritaet Thread 2 : "+thread2.getPriority()+"\n"); try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; thread1.stop(); thread2.stop(); // Threads werden gestoppt, } // wenn sie nicht beendet sind } Bildschirmausgabe Monitore: 113 6 Beispielprogramme 6.1.6 Sperr-Objekt Dieses Programm enthält zusätzlich zu den beiden Threads eine Ausgabeklasse und eine Klasse mit dem Naman SteuerObj. Die Ausgabe der beiden Threads erfolgt über die Methode eines Objektes der Klasse Ausgabe. Diese Methode besteht aus einem nicht synchronisierten und einen synchronisierten Teil. Die Synchronisation des synchronisierten Teiles wird mit Hilfe eines Sperr-Objektes, ein Objekt der Klasse SteuerObj, synchronisiert. Die Bildschirmausgabe ist auf den ersten Blick etwas verwirrend, bei genauerer Betrachtung stellt man aber fest, daß die unsynchronisierten Teile der Ausgabe beliebig ausgegeben werden. Die synchronisierten Teile sperren sich aber gegenseitig so, daß eine synchronisierte Ausgabe nie von einer anderen synchronisierten Ausgabe unterbrochen wird. Source-Code Sperr-Objekt (Dateiname SyncObjekt.java): import java.io.*; class CountingThread extends Thread { Ausgabe ausg; // Klasse wird von Thread abgeleitet public CountingThread(Ausgabe ausg){ this.ausg=ausg; // Objekt zur Ausgabe wird uebernommen } public void run(){ int lauf,lauf2,hilf; // Programmcode der als Thread // abgearbeitet werden soll for(hilf=0;hilf<=3;hilf++) { ausg.put("Thread 1 : "+hilf,1); for(lauf=0;lauf<=10000;lauf++) for(lauf2=0;lauf2<=50;lauf2++); } // Ausgabe des Zaehlers // ueber das Ausgabe-Objekt // Schleife um das System // zu belasten } } 114 6 Beispielprogramme class CountingThreadRunnable implements Runnable { Ausgabe ausg; // Klasse mit dem Interface Runnable public CountingThreadRunnable(Ausgabe ausg){ this.ausg=ausg; // Objekt zur Ausgabe wird uebernommen } public void run(){ int lauf,lauf2,hilf; for(hilf=0;hilf<=3;hilf++) { ausg.put(" Thread 2 : "+hilf,2); // Ausgabe des Zaehlers // ueber das Ausgabe-Objekt for(lauf=0;lauf<=10000;lauf++) for(lauf2=0;lauf2<=50;lauf2++); // Schleife um das } // System zu belasten } } class SteuerObj{} // Klasse fuer das Synchronisations-Objekt class Ausgabe { // Klasse Ausgabe: Ueber ein Objekt dieser Klasse // soll die Ausgabe auf dem Bildschirm erfolgen. private String buffer; private int nummer; SteuerObj steuerobj = new SteuerObj(); // Synchromisationsobjekt wird erstellt public Ausgabe(){ } public void put (String ausgabe,int nummer){ this.buffer=ausgabe; this.nummer=nummer; // Nicht synchronisierter Teil der Methode if(nummer==1) System.out.println("Thread Nummer "+nummer+" beginnt den nebenlaeufigen Teil der Methode"); else System.out.println(" Thread Nummer "+nummer+" beginnt den nebenlaeufigen Teil der Methode"); for(int lauf=0;lauf<=10000;lauf++) for(int lauf2=0;lauf2<=10000;lauf2++); // Belastungsschleife if(nummer==1) System.out.println("Thread Nummer "+nummer+" beendet den nebenlaeufigen Teil der Methode"); else System.out.println(" Thread Nummer "+nummer+" beendet den nebenlaeufigen Teil der Methode"); 115 6 Beispielprogramme // Synchronisierter Teil der Methode synchronized (steuerobj){ // Synchronisationsobjekt // wird abgefragt if(nummer==1) System.out.println("Thread Nummer "+nummer+" beginnt den synchronisierten Teil der Methode"); else System.out.println(" Thread Nummer "+nummer+" beginnt den synchronisierten Teil der Methode"); System.out.println(ausgabe); for(int lauf=0;lauf<=10000;lauf++) for(int lauf2=0;lauf2<=30000;lauf2++); // Belastungsschleife if(nummer==1) System.out.println("Thread Nummer "+nummer+" beendet den synchronisierten Teil der Methode"); else System.out.println(" Thread Nummer "+nummer+" beendet den synchronisierten Teil der Methode"); } } } class SyncObjekt { // Main-Methode public static void main(String[] args) { int read; byte buffer[] = new byte[20]; Ausgabe ausg = new Ausgabe(); // Ausgabe-Objekt wird erzeugt System.out.println("Zum Starten und Beenden bitte Eingabetaste druecken"); try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; System.out.println("Threads gestartet\n"); Runnable rthread = new CountingThreadRunnable(ausg); Thread thread2 = new Thread(rthread); CountingThread thread1 = new CountingThread(ausg); // Threads werden erzeugt und das Ausgabe-Objekt uebergeben thread1.start(); thread2.start(); // Threads werden gestartet try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; thread1.stop(); thread2.stop(); // Threads werden gestoppt, } // wenn sie nicht beendet sind } 116 6 Beispielprogramme Bildschirmausgabe Sperr-Objekt: 117 6 Beispielprogramme 6.1.7 Ereignis Bei diesem Programm werden zwei Threads über ein Ereignis synchronisiert. Als Ereignis dient die Ausgabe auf dem Bildschirm. Nachdem ein Thread 3 Ausgaben auf dem Bildschirm ausgegeben hat, gibt er ein Signal, daß nun ein anderer Thread die Ausgabe benutzen darf. Source-Code Ereignis (Dateiname ZweiThreadsEreignisse.java): import java.io.*; class CountingThread extends Thread { Ausgabe ausg; // Klasse wird von Thread abgeleitet public CountingThread(Ausgabe ausg){ this.ausg=ausg; // Objekt zur Ausgabe wird uebernommen } public void run(){ int lauf,lauf2,hilf; // Programmcode der als Thread // abgearbeitet werden soll for(hilf=0;hilf<=10;hilf++) { ausg.put1("Thread 1 : "+hilf); // Ausgabe des Zaehlers for(lauf=0;lauf<=10000;lauf++) // ueber das Ausgabe-Objekt for(lauf2=0;lauf2<=10000;lauf2++); } ausg.set(false); ausg.put1(""); // Beenden der Synchronisierung, da dieser } // Thread beendet ist und so nur noch max. 1 } // Thread auf das Ausgabe-Objekt zugreift class CountingThreadRunnable implements Runnable { Ausgabe ausg; // Klasse mit dem Interface Runnable public CountingThreadRunnable(Ausgabe ausg){ this.ausg=ausg; // Objekt zur Ausgabe wird uebernommen } public void run(){ int lauf,lauf2,hilf; for(hilf=0;hilf<=10;hilf++) { // Ausgabe des Zaehlers ueber ausg.put2(" Thread 2 : "+hilf); // das Ausgabe-Objekt for(lauf=0;lauf<=10000;lauf++) for(lauf2=0;lauf2<=6000;lauf2++); } ausg.set(false); // Beenden der Synchronisierung, da dieser ausg.put1(""); // Thread beendet ist und so nur noch max. ein } // Thread auf das Ausgabe-Objekt zugreift } 118 6 Beispielprogramme class Ausgabe { // Klasse Ausgabe: Ueber ein Objekt dieser Klasse // soll die Ausgabe auf dem Bildschirm erfolgen. private private private boolean String buffer; int anzahl=0; int soll=3; // th1 = true; // // boolean sync = true; // // Anzahl der Ausgaben Variable die angibt, welcher Thread den Monitor erhaelt Synchronisation wird auf true initalisiert public Ausgabe(){ } public void set(boolean sync){ this.sync=sync; } // Methode ueber die eine // synchronisierte Ausgabe // beendet werden kann public synchronized void put1 (String ausgabe){ // Synchronisierte Methode if (sync) // Stellt fest, ob synchronisiert werden soll { if (!th1) // Wenn th1 == false muß "thread1" warten try {wait();} catch(InterruptedException e) {} this.buffer=ausgabe; System.out.println(buffer); // Ausgabe auf Bildschirm anzahl=anzahl+1; if (anzahl>=soll) // Anzahl der Ausgaben wird mit { // dem Sollwert verglichen anzahl=0; th1=false; // Variable fuer den Monitor wird umgestellt notify(); // Eventuell wartender Thread erhaelt die } // Benachrichtigung, daß er jetzt weiter } // arbeiten kann else { this.buffer=ausgabe; if(!buffer.equals("")) System.out.println(buffer); // Ausgabe auf dem Bildschirm notify(); // Eventuell wartender Thread erhaelt die } // Benachrichtigung, daß er jetzt weiter } // arbeiten kann 119 6 Beispielprogramme public synchronized void put2 (String ausgabe){ // Synchronisierte Methode if (sync) // Stellt fest, ob synchronisiert werden soll { if (th1) // Wenn th1 == true muß "thread2" warten try {wait();} catch(InterruptedException e) {} this.buffer=ausgabe; System.out.println(buffer); anzahl=anzahl+1; if (anzahl>=soll) { anzahl=0; th1=true; notify(); } // Ausgabe auf dem Bildschirm // Anzahl der Ausgaben wird mit // dem Sollwert verglichen // // // // Variable fuer den Monitor wird umgestellt Eventuell wartender Thread erhaelt die Benachrichtigung, daß er jetzt weiter arbeiten kann } else { this.buffer=ausgabe; if(!buffer.equals("")) System.out.println(buffer); notify(); // Eventuell wartender Thread erhaelt die } // Benachrichtigung, daß er jetzt weiter } // arbeiten kann } class ZweiThreadsEreignisse { // Main-Methode public static void main(String[] args) { int read; byte buffer[] = new byte[20]; Ausgabe ausg = new Ausgabe(); // Ausgabe-Objekt wird erzeugt System.out.println("Zum Starten und Beenden bitte Eingabetaste druecken"); try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; System.out.println("Threads gestartet\n"); Runnable rthread = new CountingThreadRunnable(ausg); Thread thread2 = new Thread(rthread); CountingThread thread1 = new CountingThread(ausg); // Threads werden erzeugt und das Ausgabe-Objekt uebergeben thread1.start(); // Threads werden gestartet thread2.start(); System.out.println("Prioritaet Thread 1 : "+thread1.getPriority()); System.out.println("Prioritaet Thread 2 : "+thread2.getPriority()+"\n"); try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; thread1.stop(); thread2.stop(); // Beide Threads werden gestoppt, } // wenn sie nicht beendet sind } 120 6 Beispielprogramme Bildschirmausgabe Ereignis: 121 6 Beispielprogramme 6.1.8 Reihenfolge Bei diesem Programm werden zwei Threads erzeugt und gestartet. Thread 2 beginnt sofort mit seiner Ausführung. Thread 1 wartet bis Thread 2 beendet ist um dann ebenfalls seinen Programmcode auszuführen. Source-Code Reihenfolge (Dateiname AufEndeThread.java): import java.io.*; class CountingThread extends Thread { CountingThread(){}; // Klasse wird von Thread abgeleitet public void start(Thread thread){ try{thread.join();} catch(InterruptedException e) {} super.start(); } // // // // Start-Methode damit der Thread wartet bis der uebergebene Thread beendet ist public void run(){ int lauf,lauf2,hilf; for(hilf=0;hilf<=10;hilf++) { System.out.println("Thread 1 : "+hilf); // Ausgabe des Zaehlers for(lauf=0;lauf<=10000;lauf++) // auf dem Bildschirm for(lauf2=0;lauf2<=10000;lauf2++); } } } class CountingThreadRunnable implements Runnable { // Klasse mit dem Interface Runnable CountingThreadRunnable(){}; public void run(){ int lauf,lauf2,hilf; for(hilf=0;hilf<=10;hilf++) { System.out.println(" Thread 2 : "+hilf); for(lauf=0;lauf<=10000;lauf++) for(lauf2=0;lauf2<=6000;lauf2++); } } } 122 6 Beispielprogramme class AufEndeThread { // Main-Methode public static void main(String[] args) { int read; byte buffer[] = new byte[20]; System.out.println("Zum Starten und Beenden bitte Eingabetaste druecken"); try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; System.out.println("Threads gestartet\n"); Runnable rthread = new CountingThreadRunnable(); Thread thread2 = new Thread(rthread); CountingThread thread1 = new CountingThread(); // Erzeugen der beiden Threads System.out.println("Prioritaet Thread 1 : "+thread1.getPriority()); System.out.println("Prioritaet Thread 2 : "+thread2.getPriority()+"\n"); thread2.start(); // Thread, auf den gewartet werden // soll, wird zuerst gestartet. thread1.start(thread2); // // // // Thread, der warten soll wird gestartet. Ihm wird der Thread uebergeben "thread2" auf den er warten soll. try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; thread1.stop(); thread2.stop(); // Threads werden gestoppt, } // wenn sie nicht beendet sind } 123 6 Beispielprogramme Bildschirmausgabe Reihenfolge: 124 6 Beispielprogramme 6.1.9 Pipe Die Erzeugung und Verwendung einer Pipe zur Kommunikation zwischen zwei Threads soll in diesem Programm möglichst einfach erläutert werden. Source-Code Pipe (Dateiname Pipe.java): import java.io.*; class CountingThread extends Thread { PipedInputStream pipein; // Klasse wird von Thread abgeleitet public CountingThread(PipedInputStream pipein){ // Konstruktor this.pipein = pipein; // Stream Objekt zum Lesen }; // der Pipe wird uebernommen public void run(){ int lauf,lauf2,hilf; int ende=0; byte lese[]=new byte[1]; int ausgabe; String umwandeln; // Hilfsvariablen // // // // Feld um das augelesene Byte zu speichern Hilfsvariablen zum Umformen der Ausgabe while(true) { try {ende=pipein.read(lese,0,1);} // Pipe wird ausgelesen catch (IOException e) {} if(ende==-1) break; // bis -1 zurueckgegeben wird umwandeln=""+lese[0]; // Daten aus der Pipe ausgabe = Byte.parseByte(umwandeln); // werden fuer die // Ausgabe umgewandelt System.out.println("Thread 1 : "+ausgabe+" aus der Pipe ausgelesen"); for(lauf=0;lauf<=10000;lauf++) for(lauf2=0;lauf2<=100;lauf2++); } try {pipein.close();} catch(IOException e){} // Schleife um das System // zu belasten // Schliesst den Stream } } 125 6 Beispielprogramme class CountingThreadRunnable implements Runnable { // Klasse mit dem Interface Runnable PipedOutputStream pipeout; public CountingThreadRunnable(PipedOutputStream pipeout){ // Konstruktor this.pipeout=pipeout; // Stream Objekt zum Schreiben }; // in die Pipe wird uebernommen public void run(){ int lauf,lauf2,hilf; String umwandeln; byte schreibe[] = new byte[1] ; for(hilf=0;hilf<=10;hilf++) { // 10 mal wird eine Zahl in // die Pipe geschrieben umwandeln=""+hilf; // Zahl wird zum schreibe[0]=Byte.parseByte(umwandeln); // Schreiben umgewandelt try {pipeout.write(schreibe,0,1);} catch(IOException e){} System.out.println(" // Daten werden in die // Pipe geschrieben Thread 2 : "+hilf+" in die Pipe geschrieben"); for(lauf=0;lauf<=10000;lauf++) for(lauf2=0;lauf2<=90;lauf2++); // Schleife um das System // zu belasten try {pipeout.close();} catch(IOException e){} // Schliesst den Stream } } } 126 6 Beispielprogramme class Pipe { // Main-Methode public static void main(String[] args) { int read; byte buffer[] = new byte[20]; System.out.println("Zum Starten und Beenden bitte Eingabetaste druecken"); try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; PipedInputStream pipein = new PipedInputStream(); PipedOutputStream pipeout = new PipedOutputStream(); // Erzeugen der Stream-Objekte zum Schreiben und Lesen try {pipeout.connect(pipein);} catch (IOException e) {} // Kopplung der Streams System.out.println("Threads gestartet\n"); Runnable rthread = new CountingThreadRunnable(pipeout); Thread thread2 = new Thread(rthread); CountingThread thread1 = new CountingThread(pipein); // Erzeugung der Threads mit Uebergabe der Stream-Objekte thread1.start(); thread2.start(); // Starten der Threads try {read = System.in.read(buffer,0,20);} catch (IOException e ){}; thread1.stop(); thread2.stop(); // Threads werden gestoppt, } // wenn sie nicht beendet sind } Bildschirmausgabe Pipe: 127 6 Beispielprogramme 6.2 Beispielprogramme für Windows NT 6.2.1 Counter Das Programm Counter ist ein Zähler, der die Zahlen 0 bis 10 auf dem Bildschirm ausgibt. Source-Code Counter (Dateiname: Counter.cpp): #include <iostream.h> #include <conio.h> main () { int ein; int lauf,lauf2,hilf; // Variable zum Starten des Programmes // Hilfsvariablen fuer Schleife clrscr(); cout << "\nZum Starten und Beenden bitte Taste druecken\n\n"; ein = getch(); for(hilf=0;hilf<=10;hilf++) { cout << "Counter : " << hilf << endl; for (lauf=0;lauf<=100000;lauf++) for(lauf2=0;lauf2<=1000;lauf2++); // Ausgabe der // Zaehlerschleife // Schleife um das // System zu belasten } ein = getch(); return 0 ; } Bildschirmausgabe Counter: 128 6 Beispielprogramme 6.2.2 Thread Das Programm Thread ist, wie das Programm Counter, ein Zähler. Der Unterschied zwischen den beiden Programmen liegt darin, daß bei dem Programm Thread der Programmcode für den Zähler als Thread programmiert wurde. Dadurch kann das Programm Thread vom Anwender unterbrochen werden. Source-Code Thread (Dateiname Thread.cpp): #include <windows.h> #include <iostream.h> #include <conio.h> long WINAPI Print( long lParam ) // Funktion, die der { // Thread durchlaeuft int zahl=0,lauf,lauf2; while(zahl<=10) { cout << "Thread # " << lParam << " Zahl = " << zahl << endl; zahl=zahl+1; for (lauf=0;lauf<=100000;lauf++) // Schleife um das for(lauf2=0;lauf2<=1000;lauf2++); // System zu belasten } return NO_ERROR; } main () { int ein; int prio; unsigned long nThreadID; // Variable zum Starten des Programmes // Variable fuer Prioritaet // ID Nummer clrscr(); cout << "\nZum Starten und Beenden bitte Taste druecken\n\n"; ein = getch(); HANDLE hThread = CreateThread(NULL, // Thread wird erzeugt 0, (LPTHREAD_START_ROUTINE)Print, ( void*)1, 0, &nThreadID); prio = GetThreadPriority( hThread ); cout << "Prioritaet: " << prio << " relative Prioritaet" " innerhalb des Prozesses\n" << endl; ein = getch(); CloseHandle(hThread); return 0; } // Thread-Handle wird geschlossen 129 6 Beispielprogramme Bildschirmausgabe Thread: 130 6 Beispielprogramme 6.2.3 Zwei Threads Bei diesem Programm wurden zwei Zähler als Threads programmiert. Beide Zähler laufen völlig unabhängig von einander. Source-Code Zwei Threads (Dateiname 2Threads.cpp): #include <windows.h> #include <iostream.h> #include <conio.h> long WINAPI Print( long lParam ) { int zahl=0,lauf,lauf2; // Funktion, die der // 1. Thread durchlaeuft while(zahl<=10) { cout << "Thread # " << lParam << " Zahl = " << zahl << endl; zahl=zahl+1; for (lauf=0;lauf<=100000;lauf++) // Schleife um Prozessor for(lauf2=0;lauf2<=1000;lauf2++); // zu belasten } return NO_ERROR; } long WINAPI Print2( long lParam ) { int zahl=0,lauf,lauf2; // Funktion, die der // 2. Thread durchlaeuft while(zahl<=10) { cout << "\tThread # " << lParam << " Zahl = " << zahl << endl; zahl=zahl+1; for (lauf=0;lauf<=100000;lauf++) // Schleife um Prozessor for(lauf2=0;lauf2<=1500;lauf2++); // zu belasten } return NO_ERROR; } main () { int ein; int prio; unsigned long nThreadID; clrscr(); cout << "\nZum Starten und Beenden bitte Taste druecken\n\n"; ein = getch(); 131 6 Beispielprogramme HANDLE hThread = CreateThread(NULL, // 1. Thread wird erzeugt 0, (LPTHREAD_START_ROUTINE)Print, ( void*)1, 0, &nThreadID); HANDLE bThread = CreateThread(NULL, // 2. Thread wird erzeugt 0, (LPTHREAD_START_ROUTINE)Print2, ( void*)2, 0, &nThreadID); prio = cout << << prio = cout << << GetThreadPriority( hThread ); // Prioritaet wird "Prioritaet Thread 1: " << prio // ermittelt und ausg. " relative Prioritaet innerhalb des Prozesses\n" << endl; GetThreadPriority( bThread ); "Prioritaet Thread 2: " << prio " relative Prioritaet innerhalb des Prozesses\n" << endl; ein = getch(); CloseHandle(hThread); CloseHandle(bThread); return 0 ; } // Thread-Handle hThread wird geschlossen // Thread-Handle bThread wird geschlossen Bildschirmausgabe Zwei Threads: 132 6 Beispielprogramme 6.2.4 Priorität In diesem Programm werden zwei Zähler als Threads gestartet. Beide Threads besitzen am Anfang die gleiche Priorität. Nach einiger Zeit ( 2-3 Sekunden ) wird die Priorität des 1. Threads um 2 herabgesetzt und die Priorität des 2. Threads um 1 erhöht. Source-Code Priorität (Dateiname Priorität.cpp): #include <windows.h> #include <iostream.h> #include <conio.h> long WINAPI Print( long lParam ) { int zahl=0,lauf,lauf2; // Funktion, die der // 1. Thread durchlaeuft while(zahl<=10) { cout << "Thread # " << lParam << " Zahl = " << zahl << endl; zahl=zahl+1; for (lauf=0;lauf<=100000;lauf++) // Schleife um Prozessor for(lauf2=0;lauf2<=1000;lauf2++); // zu belasten } return NO_ERROR; } long WINAPI Print2( long lParam ) { int zahl=0,lauf,lauf2; // Funktion, die der // 2. Thread durchlaeuft while(zahl<=10) { cout << "\tThread # " << lParam << " Zahl = " << zahl << endl; zahl=zahl+1; for (lauf=0;lauf<=100000;lauf++) // Schleife um Prozessor for(lauf2=0;lauf2<=1500;lauf2++); // zu belasten } return NO_ERROR; } main () { int ein; int prio; unsigned long nThreadID; clrscr(); cout << "\nZum Starten und Beenden bitte Taste druecken\n\n"; ein = getch(); 133 6 Beispielprogramme HANDLE hThread = CreateThread (NULL, // 1. Thread wird erzeugt 0, (LPTHREAD_START_ROUTINE)Print, ( void*)1, 0, &nThreadID); HANDLE bThread = CreateThread (NULL, // 2. Thread wird erzeugt 0, (LPTHREAD_START_ROUTINE)Print2, ( void*)2, 0, &nThreadID); prio = cout << << prio = cout << << GetThreadPriority( hThread ); "Prioritaet Thread 1: " << prio " relative Prioritaet innerhalb GetThreadPriority( bThread ); "Prioritaet Thread 2: " << prio " relative Prioritaet innerhalb // Prioritaet wird // ermittelt und ausg. des Prozesses" << endl; des Prozesses\n" << endl; Sleep (3000); cout << "\nDie Prioritaet von beiden Threads werden geaendert\n"; SetThreadPriority( hThread, THREAD_PRIORITY_LOWEST ); // Prioritaet vom Thread mit dem // Handle hThread wird um 2 herabgestuft SetThreadPriority( bThread, THREAD_PRIORITY_ABOVE_NORMAL ); // Prioritaet vom Thread mit dem // Handle bThread wird um 1 heraufgesetzt prio = cout << << prio = cout << << GetThreadPriority( hThread ); "Prioritaet Thread 1: " << prio " relative Prioritaet innerhalb des Prozesses" << endl; GetThreadPriority( bThread ); "Prioritaet Thread 2: " << prio " relative Prioritaet innerhalb des Prozesses\n" << endl; // neue Prioritaet der Threads wird // ermittelt und ausgegeben ein = getch(); CloseHandle(hThread); CloseHandle(bThread); return 0; } // Thread-Handle hThread wird geschlossen // Thread-Handle bThread wird geschlossen 134 6 Beispielprogramme Bildschirmausgabe Priorität: 135 6 Beispielprogramme 6.2.5 Ereignis Bei diesem Programm werden zwei Threads über ein Ereignis synchronisiert. Als Ereignis dient die Ausgabe auf dem Bildschirm. Nachdem ein Thread 4 Ausgaben auf dem Bildschirm ausgegeben hat, gibt er ein Signal, daß nun ein anderer Thread die Ausgabe benutzen darf. Source-Code Ereignis (Dateiname Ereignis.cpp): #include <windows.h> #include <iostream.h> #include <conio.h> long WINAPI Print( long lParam ) { int zahl=0,lauf,lauf2,ereignisse=0; // Funktion, die der // 1. Thread durchlaeuft HANDLE hEvent = OpenEvent(EVENT_ALL_ACCESS,FALSE,"EventName"); // Handle auf das Ereignis while(zahl<=10) // wird geoeffnet { WaitForSingleObject(hEvent,INFINITE); // Auf das Ereignis // hEvent wird gewartet while (ereignisse < 4 && zahl<=10) { cout << "Thread # " << lParam << " Zahl = " << zahl << endl; zahl++; ereignisse++; // Ereignisse werden gezaehlt for (lauf=0;lauf<=100000;lauf++) // Schleife um Prozessor for(lauf2=0;lauf2<=1000;lauf2++); // zu belasten } SetEvent(hEvent); // Ereignis wird auf signalisierend gesetzt ereignisse=0; // wenn 4 Bildschirmausgaben erfolgten } SetEvent(hEvent); // Ereignis wird auf signalisierend gesetzt return NO_ERROR; // wenn die Funktion beendet wird } 136 6 Beispielprogramme long WINAPI Print2( long lParam ) { int zahl=0,lauf,lauf2,ereignisse=0; // Funktion, die der // 2. Thread durchlaeuft HANDLE hEvent = OpenEvent(EVENT_ALL_ACCESS,FALSE,"EventName"); // Handle auf das Ereignis while(zahl<=10) // wird geoeffnet { if (zahl!=0) // Hierdurch wird festgelegt, daß der // 2. Thread mit der Ausgabe beginnt WaitForSingleObject(hEvent,INFINITE); // Auf das Ereignis // hEvent wird gewartet while (ereignisse < 4 && zahl<=10) { cout << "\tThread # " << lParam << " Zahl = " << zahl << endl; zahl++; ereignisse++; for (lauf=0;lauf<=100000;lauf++) // Schleife um Prozessor for(lauf2=0;lauf2<=1500;lauf2++); // zu belasten } SetEvent(hEvent); // Ereignis wird auf signalisierend gesetzt ereignisse=0; // wenn 4 Bildschirmausgaben erfolgten } SetEvent(hEvent); // Ereignis wird auf signalisierend gesetzt return NO_ERROR; // wenn die Funktion beendet wird } main () { int ein; int prio; unsigned long nThreadID; clrscr(); cout << "\nZum Starten und Beenden bitte Taste druecken\n\n"; ein = getch(); HANDLE hEvent = CreateEvent (NULL, // Ereignis wird erzeugt FALSE, FALSE, "EventName"); HANDLE hThread = CreateThread(NULL, // 1. Thread wird erzeugt 0, (LPTHREAD_START_ROUTINE)Print, ( void*)1, 0, &nThreadID); HANDLE bThread = CreateThread(NULL, // 2. Thread wird erzeugt 0, (LPTHREAD_START_ROUTINE)Print2, ( void*)2, 0, &nThreadID); 137 6 Beispielprogramme prio = cout << << prio = cout << << GetThreadPriority( hThread ); // Prioritaet wird "Prioritaet Thread # 1: " << prio // ermittelt und ausg. " relative Prioritaet innerhalb des Prozesses" << endl; GetThreadPriority( bThread ); "Prioritaet Thread # 2: " << prio " relative Prioritaet innerhalb des Prozesses\n" << endl; ein = getch(); CloseHandle(hEvent); CloseHandle(hThread); CloseHandle(bThread); return 0; } // Handles werden geschlossen Bildschirmausgabe Ereignis: 138 6 Beispielprogramme 6.2.6 Semaphor Das Programm erzeugt einen Semaphor, der auf 3 initialisiert wird, einen kritischen Bereich und 10 Threads. Alle 10 Threads versuchen nun den Semaphor zu dekrementieren, was den ersten 3 Threads auch gelingt. Alle anderen Threads müssen warten, bis ein anderer Thread den Semaphor inkrementiert. Der kritische Bereich wird bei diesem Programm benötigt, damit der Wert des Semaphores, der durch eine einfache Variable dargestellt wird, von einem Thread, verändert und ausgegeben werden kann ohne das ein anderer Thread zwischenzeitlich diese Variable verändert. Source-Code Semaphor (Dateiname Semaphor.cpp): #include <windows.h> #include <iostream.h> #include <conio.h> CRITICAL_SECTION csOutput; int sem=3; // Variable fuer den // kritischen Bereich // Variable um Zaehlerstand des Semaphors anzuzeigen long WINAPI Print( long lParam ) { // Funktion, die die // Threads durchlaufen HANDLE semaphor = OpenSemaphore(SEMAPHORE_ALL_ACCESS, NULL, "Semap"); // Handle fuer Semaphor wird geoeffnet cout << "Kunde "<< lParam << " betritt den Laden" <<endl; WaitForSingleObject(semaphor,INFINITE); // Semaphor wird abgefragt und wenn moeglich herabgesetzt __try { // Eintritt in den kritischen Bereich EnterCriticalSection( &csOutput ); sem--; cout << "Kunde "<< lParam << " wird bedient" << " Semaphor: "<<sem<<endl; } __finally { LeaveCriticalSection( &csOutput ); } // Verlassen des kritischen Bereiches 139 6 Beispielprogramme Sleep((rand() %3000)+2000); __try { // Eintritt in den kritischen Bereich EnterCriticalSection( &csOutput ); sem++; cout << "Kunde "<< lParam << " verlaesst den Laden" << " Semaphor: "<<sem<<endl; } __finally { LeaveCriticalSection( &csOutput ); } // Verlassen des kritischen Bereiches ReleaseSemaphore(semaphor,1,NULL); return NO_ERROR; } // Semaphor wird um 1 erhoeht int main() { int ein; unsigned long nThreadID; clrscr(); // Variable fuer den kritichen Bereich InitializeCriticalSection(&csOutput); // wird initialisiert cout << "\nZum Starten und Beenden bitte Taste druecken\n\n"; ein = getch(); HANDLE semaphor = CreateSemaphore (NULL, // Semaphor wird erzeugt 3, 3, "Semap"); for( int cThread=0; cThread<10; cThread++) { // 10 Threads werden erzeugt CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Print, (void*)cThread, 0, &nThreadID); } ein = getch(); CloseHandle(semaphor); // Handle des Semaphors wird geschlossen return 0; } 140 6 Beispielprogramme Bildschirmausgabe Semaphor: 141 6 Beispielprogramme 6.2.7 Philosophen Quelle [HaWi1997] Das Problem der "speisende Philosophen" wird sehr häufig verwendet um die Problematik einer Synchronisation darzustellen. Dieses Programm verwendet Mutexe und kritische Bereiche um die Threads zu synchronisieren. Source-Code Philosophen (Dateiname Phiol.cpp): #include <iostream.h> #include <windows.h> #include <time.h> long g_fDone = FALSE; struct CPhilosopher { int m_nID; HANDLE m_hForks[2]; int m_nMeals; }; CRITICAL_SECTION csOutput; // Variable des kritischen Bereiches void SayEat( CPhilosopher* pPhilo ) { __try { // Eintritt in den kritischen Bereich EnterCriticalSection( &csOutput ); cout << "Philosopher " << pPhilo->m_nID << " eats a while" << endl; } __finally { // Verlassen des kritischen Bereiches LeaveCriticalSection( &csOutput ); } pPhilo->m_nMeals++; } void SayThink( int nPhilo ) { __try { // Eintritt in den kritischen Bereich EnterCriticalSection( &csOutput ); cout << "Philosopher " << nPhilo << " thinks a while" << endl; } __finally { // Verlassen des kritischen Bereiches LeaveCriticalSection( &csOutput ); } } 142 6 Beispielprogramme void SayDone( CPhilosopher* pPhilo ) { __try { // Eintritt in den kritischen Bereich EnterCriticalSection( &csOutput ); cout << "Philosopher " << pPhilo->m_nID << " had " << pPhilo->m_nMeals << " meals.(burp)" << endl; } __finally { // Verlassen des kritischen Bereiches LeaveCriticalSection( &csOutput ); } } long WINAPI WaitToEat( long lParam ) { CPhilosopher* pPhilo = (CPhilosopher*)lParam; while( g_fDone == FALSE ) { WaitForMultipleObjects(2, pPhilo->m_hForks, TRUE, INFINITE ); // // // // Thread wartet bis beide Gabeln frei sind SayEat( pPhilo ); Sleep( (rand() % 1000) + 1000 ); SayThink( pPhilo->m_nID ); ReleaseMutex(pPhilo->m_hForks[1]); Sleep(0); ReleaseMutex(pPhilo->m_hForks[0]); Sleep( (rand() % 1000) + 1000 ); } SayDone( pPhilo ); return 0; } // // // // Thread gibt rechte Gabel zurueck Thread gibt linke Gabel zurueck int main() { const int nMaxPhil = 3; // Anzahl der Philosophen am Tisch srand((unsigned)time(NULL)); HANDLE CPhilosopher unsigned long rghFork[nMaxPhil]; rgPhilosophers[nMaxPhil]; nThread; // kritischer Bereich InitializeCriticalSection( &csOutput ); // wird initialisiert 143 6 Beispielprogramme for( int nFork = 0; nFork < nMaxPhil; nFork++ ) rghFork[nFork] = CreateMutex(NULL,FALSE,NULL); // Fuer jede Gabel wird ein Mutex erzeugt for( int nPhilo = 0; nPhilo < nMaxPhil; nPhilo++ ) { rgPhilosophers[nPhilo].m_nID = nPhilo; rgPhilosophers[nPhilo].m_nMeals = 0; rgPhilosophers[nPhilo].m_hForks[0] = rghFork[nPhilo]; if( nPhilo < nMaxPhil-1 ) rgPhilosophers[nPhilo].m_hForks[1] = rghFork[nPhilo+1]; else rgPhilosophers[nPhilo].m_hForks[1] = rghFork[0]; HANDLE hThread = CreateThread(NULL, // Threads werden erzeugt 0, (LPTHREAD_START_ROUTINE)WaitToEat, (void*)&rgPhilosophers[nPhilo], 0, &nThread ); CloseHandle( hThread ); } Sleep( 10000 ); g_fDone = TRUE; Sleep( 8000 ); DeleteCriticalSection( &csOutput ); return 0; } // Handle der Threads // werden geschlossen // kritischer Bereich // wird geloescht Bildschirmausgabe Philosophen: 144 6 Beispielprogramme 6.2.8 Ereignis für zwei Prozesse In Windows NT können nicht nur Threads mit der Hilfe von Ereignissen synchronisiert werden sondern auch Prozesse. Die beiden Beispielprogramme Wait Event und Sig Event sollen eine solche Synchronisation darstellen. Source-Code Wait Event (Dateiname WaitEvent.cpp): #include <iostream.h> #include <windows.h> int main() { // Ereignis wird erzeugt HANDLE hEvent = CreateEvent( NULL, FALSE, FALSE, "WaitEvent" ); cout << "Warte auf SigEvent" << endl; WaitForSingleObject( hEvent, INFINITE ); // Auf das Ereignis // wird gewartet cout << "Event handle gefunden" << endl; CloseHandle( hEvent ); // Ereignis-Handle wird geschlossen return 0; } Source-Code Sig Event (Dateiname SigEvent.cpp): #include <iostream.h> #include <windows.h> int main() { // Oeffnet den Handle fuer das Ereignis HANDLE hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE,"WaitEvent"); cout << "Signalisiere WaitEvent-Handle" << endl; SetEvent( hEvent ); return 0; } // Setzt das Ereignis auf signalisierend 145 6 Beispielprogramme Bildschirmausgabe der Prozesse Wait Event und Sig Event: 146 6 Beispielprogramme 6.2.9 Unbenannte Pipe Die Erzeugung und Verwendung einer unbenannten Pipe zur Kommunikation zwischen zwei Threads soll in diesem Programm möglichst einfach erläutert werden. Source-Code unbenannte Pipe (Dateiname unbPipe.cpp): #include <windows.h> #include <iostream.h> #include <conio.h> const int cBuffer = 255; HANDLE hRead,hWrite; // Buffergroesse der Pipe // Handle zum Lesen, Schreiben long WINAPI Put() // Funktion, die der Schreibthread durchlaeuft { int zahl=1; TCHAR Msg[cBuffer]; DWORD dwWritten; while(zahl<=10) { itoa(zahl,Msg,10); BOOL fWritten = WriteFile (hWrite, // schreiben in die Pipe Msg, sizeof(Msg), &dwWritten, NULL ); if( fWritten == FALSE || dwWritten == 0 ) { cout << "Fehler beim Schreiben" << endl ; break; } else cout << " Der Wert: " <<Msg<<" wurde gesendet" <<endl; Sleep((rand() %1000)+500); zahl++; } return NO_ERROR; } 147 6 Beispielprogramme long WINAPI Get() // Funktion, die der Lesethread durchlaeuft { TCHAR Buffer[cBuffer]; DWORD dwRead; while(1) { BOOL fRead = ReadFile (hRead, // Auslesen der Pipe Buffer, sizeof (Buffer), &dwRead, NULL ); if( fRead == FALSE || dwRead == 0 ) { cout << "Fehler beim Lesen" << endl; break; } cout << " Der Wert: " << Buffer << " wurde ausgelesen " << endl; Sleep((rand() %1000)+500); } return NO_ERROR; } int main() { int ein; unsigned long nThreadID; clrscr(); cout << "\nZum Starten und Beenden bitte Taste druecken\n"; ein = getch(); BOOL fCreated = CreatePipe (&hRead, &hWrite, NULL, 0); // Pipe wird erzeugt if( !fCreated ) cout << "Fehler beim Erstellen der Pipe" << endl; else cout << "Pipe wurde erstellt" << endl ; Sleep(2000); HANDLE bThread = CreateThread (NULL, //Schreibthread wird erzeugt 0, (LPTHREAD_START_ROUTINE)Put, ( void*)1, 0, &nThreadID); CloseHandle(bThread); 148 6 Beispielprogramme cout << "Thread zum schreiben in die Pipe gestartet " << endl; Sleep(2000); HANDLE hThread = CreateThread(NULL, // Lesethread wird erzeugt 0, (LPTHREAD_START_ROUTINE)Get, (void*)2, 0, &nThreadID); CloseHandle(hThread); cout << "Thread zum Auslesen der Pipe gestartet " << endl; ein = getch(); CloseHandle(hWrite); CloseHandle(hRead); return 0; } // Schreib- und Lesehandle // werden geschlossen Bildschirmausgabe unbenannte Pipe: 149 6 Beispielprogramme 6.2.10 Benannte Pipe eine Richtung Dieses Programm soll die Verwendung einer benannten Pipe zur Kommunikation zwischen zwei Threads zeigen. In diesem Beispiel erfolgt die Kommunikation unidirektional. Source-Code benannte Pipe eine Richtung (Dateiname benPipe1R.cpp): #include <windows.h> #include <iostream.h> #include <conio.h> const int cBuffer = 255; const int cTimeout = INFINITE; const TCHAR Pipe[] = TEXT("\\\\.\\pipe\\Quote"); // Pipename long WINAPI Put() // Funktion, die der { // Schreibthread durchlaeuft int zahl=1; DWORD dwWritten; TCHAR Msg[cBuffer]; HANDLE hPipe = CreateFile (Pipe, // Pipe Handle wird geoeffnet GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); DWORD dwPipeState = PIPE_READMODE_MESSAGE; BOOL fChangedState = SetNamedPipeHandleState (hPipe, &dwPipeState, NULL, NULL ); // Pipe wird in Nachrichten-Pipe umgewandelt if( fChangedState == FALSE ) { cout << "Fehler beim Umformen der Pipe" << endl; } else cout << "Byte-Pipe in Nachrichten-Pipe umgeformt" << endl; 150 6 Beispielprogramme while(zahl<=10) { itoa(zahl,Msg,10); BOOL fWritten = WriteFile (hPipe, // Schreiben in die Pipe Msg, lstrlen(Msg) + 1, &dwWritten, NULL ); if( fWritten == FALSE || dwWritten == 0 ) { cout << "Fehler beim Schreiben" << endl ; break; } else cout<<" Der Wert: "<<Msg<<" wurde gesendet"<<endl; Sleep((rand() %1000)+500); zahl++; } CloseHandle(hPipe); // Handle der Pipe wird geschlossen return NO_ERROR; } long WINAPI Get( long lParam ) // Funktion, die der Lesethread { // durchlaeuft Pipe Handle TCHAR Buffer[cBuffer]; // wird uebergeben DWORD dwRead; HANDLE hPipe =(HANDLE)lParam; while(1) { BOOL fRead = ReadFile (hPipe, // Auslesen der Pipe Buffer, cBuffer, &dwRead, NULL ); if( fRead == FALSE || dwRead == 0 ) { cout << "Fehler beim Lesen" << endl; break; } cout << " Der Wert: " << Buffer << " wurde ausgelesen " << endl; if(lstrcmpi(TEXT("10"),Buffer)==0)break; Sleep((rand() %1000)+500); } CloseHandle(hPipe); // Handle der Pipe wird geschlossen return NO_ERROR; } 151 6 Beispielprogramme int main() { int ein; unsigned long nThreadID; clrscr(); cout << "\nZum Starten und Beenden bitte Taste druecken\n"; ein = getch(); HANDLE hPipe = CreateNamedPipe (Pipe, // ben. Pipe wird erzeugt PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, cBuffer, cBuffer, cTimeout, NULL ); if( hPipe == INVALID_HANDLE_VALUE ) cout << "Fehler beim Erstellen der Pipe" << endl; else cout << "Pipe wurde erstellt" << endl; Sleep(2000); HANDLE bThread = CreateThread(NULL, // Schreibthread wird erzeugt 0, (LPTHREAD_START_ROUTINE)Put, ( void*)1, 0, &nThreadID); CloseHandle(bThread); cout << "Thread zum Schreiben in die Pipe gestartet " << endl; Sleep(2000); BOOL connected = ConnectNamedPipe( hPipe, NULL ); if(connected || GetLastError() == ERROR_PIPE_CONNECTED ) { HANDLE hThread = CreateThread(NULL, // Lesethread wird erzeugt 0, (LPTHREAD_START_ROUTINE)Get, (LPVOID)hPipe, 0, &nThreadID); CloseHandle(hThread); cout << "Thread zum Auslesen der Pipe gestartet " << endl; } ein = getch(); CloseHandle(hPipe); // Handle der Pipe wird geschlossen return 0; } 152 6 Beispielprogramme Bildschirmausgabe benannte Pipe eine Richtung: 153 6 Beispielprogramme 6.2.11 Benannte Pipe zwei Richtungen Dieses Programm soll die Verwendung einer benannten Pipe zur Kommunikation zwischen zwei Threads zeigen. In diesem Beispiel erfolgt die Kommunikation bidirektional. Source-Code benannte Pipe zwei Richtungen (Dateiname benPipe2R.cpp): #include <windows.h> #include <iostream.h> #include <conio.h> const int cBuffer = 256; const int cTimeout = INFINITE; const TCHAR Pipe[] = TEXT("\\\\.\\pipe\\Quote"); // Name der Pipe long WINAPI Put1() // Funktion, die der 1. Thread durchlaeuft { int zahl=1; DWORD dwWritten,dwRead; TCHAR Msg[cBuffer]; TCHAR Buffer[cBuffer]; HANDLE hPipe = CreateFile (Pipe, // Pipe Handle wird geoeffnet GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); DWORD dwPipeState = PIPE_READMODE_MESSAGE; BOOL fChangedState = SetNamedPipeHandleState (hPipe, &dwPipeState, NULL, NULL ); // Pipe wird in Nachrichten-Pipe umgewandelt if( fChangedState == FALSE ) { cout << "Fehler beim Umformen der Pipe" << endl; } else cout << "Pipe umgeformt" << endl; Sleep(100); 154 6 Beispielprogramme while(1) { if(zahl<=10) { itoa(zahl,Msg,10); BOOL fWritten = WriteFile (hPipe, // Schreiben der Zahl Msg, // in die Pipe lstrlen(Msg) + 1, &dwWritten, NULL ); if( fWritten == FALSE || dwWritten == 0 ) { cout << "Fehler beim Schreiben" << endl; break; } else cout << "Sender Der Wert: " << Msg << " wurde gesendet" << endl; zahl++; Sleep(500); } BOOL fRead = ReadFile (hPipe, Buffer, cBuffer, &dwRead, NULL ); // Lesen der Bestaetigung // aus der Pipe if( fRead == FALSE || dwRead == 0 ) { cout << "Fehler beim Lesen" <<endl; break; } else { cout << "Sender " << Buffer <<" erhalten\n" << endl; if(lstrcmpi(TEXT("Bestaetigung fuer Wert: 10"),Buffer)==0) break; } } CloseHandle(hPipe); // Handle der Pipe wird geschlossen return NO_ERROR; } 155 6 Beispielprogramme long WINAPI Get1( long lParam ) { TCHAR Buffer[cBuffer]; TCHAR Msg[cBuffer]; DWORD dwRead,dwWritten; HANDLE hPipe =(HANDLE)lParam; while(1) { BOOL fRead = ReadFile (hPipe, Buffer, cBuffer, &dwRead, NULL ); // Funktion, die der 2. Thread // durchlaeuft Pipe Handle // wird uebergeben // Pipe wird ausgelesen cout << "Empfaenger Der Wert: " << Buffer << " wurde empfangen / schicke Bestaetigung" << endl; if( fRead == FALSE || dwRead == 0 ) { cout <<"Fehler beim Lesen" << endl; break; } else { strcpy(Msg,"Bestaetigung fuer Wert: "); strcat(Msg,Buffer); BOOL fWritten = WriteFile (hPipe, // Bestaetigung wird Msg, // in die Pipe lstrlen(Msg) + 1, // geschrieben &dwWritten, NULL ); if(lstrcmpi(TEXT("10"),Buffer)==0) break; if( fWritten == FALSE || dwWritten == 0 ) { cout << "Fehler beim Schreiben" << endl ; break; } } } CloseHandle(hPipe); // Handle der Pipe wird geschlossen return NO_ERROR; } 156 6 Beispielprogramme int main() { int ein; unsigned long nThreadID; clrscr(); cout << "\nZum Starten und Beenden bitte Taste druecken\n"; ein = getch(); HANDLE hPipe = CreateNamedPipe (Pipe, // ben. Pipe wird erzeugt PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, cBuffer, cBuffer, cTimeout, NULL ); if( hPipe == INVALID_HANDLE_VALUE ) cout << "Fehler beim Erstellen der Pipe" << endl; else cout << "Pipe wurde erstellt" << endl ; Sleep(2000); HANDLE bThread = CreateThread (NULL, // 1. Thread wird erzeugt 0, (LPTHREAD_START_ROUTINE)Put1, ( void*)1, 0, &nThreadID); CloseHandle(bThread); cout<<"Thread zum Schreiben in die Pipe gestartet "<<endl; BOOL connected = ConnectNamedPipe( hPipe, NULL ); if(connected || GetLastError() == ERROR_PIPE_CONNECTED ) { HANDLE hThread = CreateThread (NULL, // 2. Thread wird erzeugt 0, (LPTHREAD_START_ROUTINE)Get1, (LPVOID)hPipe, 0, &nThreadID); CloseHandle(hThread); cout<<"Thread zum Auslesen der Pipe gestartet \n"<<endl; } ein = getch(); CloseHandle(hPipe); // Handle der Pipe wird geschlossen return 0; } 157 6 Beispielprogramme Bildschirmausgabe benannte Pipe zwei Richtungen: 158 6 Beispielprogramme 6.2.12 Benannte Pipe für zwei Prozesse In Windows NT können benannte Pipes auch für die Kommunikation zwischen Prozesse eingesetzt werden. Dies soll durch die beiden Programme benPipeE und benPipeS gezeigt werden. Source-Code benannte Pipe Empfänger (Dateiname benPipeE.cpp): #include <windows.h> #include <iostream.h> #include <conio.h> const int cBuffer = 256; const int cTimeout = INFINITE; const TCHAR Pipe[] = TEXT("\\\\.\\pipe\\Quote"); long WINAPI Print( long lParam ) { TCHAR Buffer[cBuffer]; DWORD dwRead; HANDLE hPipe =(HANDLE)lParam; while(1) { BOOL fRead = ReadFile (hPipe, Buffer, cBuffer, &dwRead, NULL ); // Pipename // Pipe Handle wird uebergeben // Pipe wird ausgelesen if( fRead == FALSE || dwRead == 0 ) { cout << "Fehler beim Lesen" <<endl; break; } if( lstrcmpi("Quit",Buffer)==0) // Thread wird beendet, sobald { // "Quit" empfangen wird cout << "Quit empfangen beende Thread zum Auslesen der Pipe" << endl; CloseHandle(hPipe); // Handle der Pipe wird geschlossen break; } else cout << "Der Wert: " << Buffer << " wurde ausgelesen " << endl; Sleep((rand() %1000)+500); } return NO_ERROR; } 159 6 Beispielprogramme int main() { int ein; unsigned long nThreadID; clrscr(); cout << "Zum Starten und Beenden bitte Taste druecken\n\n"; ein = getch(); HANDLE hPipe = CreateNamedPipe (Pipe, // ben. Pipe wird erzeugt PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, cBuffer, cBuffer, cTimeout, NULL); if( hPipe == INVALID_HANDLE_VALUE ) cout << "Fehler beim Erstellen der Pipe" << endl; cout << "Pipe wurde erstellt" << endl ; BOOL connected = ConnectNamedPipe( hPipe, NULL ); // Thread zum Auslesen der Pipe wird // erzeugt wenn die Pipe geoeffnet wird if(connected || GetLastError() == ERROR_PIPE_CONNECTED ) { HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Print, (LPVOID)hPipe, 0, &nThreadID); CloseHandle(hThread); cout << "Thread zum Auslesen der Pipe gestartet " << endl; } ein = getch(); CloseHandle(hPipe); return 0; } // Handle der Pipe wird geschlossen 160 6 Beispielprogramme Source-Code benannte Pipe Sender (Dateiname benPipeS.cpp): #include <windows.h> #include <iostream.h> #include <conio.h> int main() { int ein; int zahl=1; const TCHAR Pipe[] = TEXT("\\\\.\\pipe\\Quote"); // Name der Pipe boolean proz=false; DWORD dwWritten; TCHAR Msg[10]; // Nachrichtenfeld HANDLE hPipe; clrscr(); cout << "Zum Starten und Beenden bitte Taste druecken\n" << endl; ein = getch(); while(proz==false) { hPipe = CreateFile (Pipe, // Pipe wird geoeffnet GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if(hPipe == INVALID_HANDLE_VALUE) { cout << "Keine Pipe vorhanden Empfaengerprozess starten" << endl; Sleep(1000); } else { proz=true; } } DWORD dwPipeState = PIPE_READMODE_MESSAGE; BOOL fChangedState = SetNamedPipeHandleState (hPipe, &dwPipeState, NULL, NULL ); // Pipe wird in eine Nachrichten-Pipe umgewandelt if( fChangedState == FALSE ) cout << "Fehler beim Umformen der Pipe " << endl; else cout << "Pipe umgeformt" << endl; 161 6 Beispielprogramme while(zahl<=11) { if (zahl==11) strcpy(Msg,"Quit"); // Nach 10 Zahlen else itoa(zahl,Msg,10); BOOL fWritten = WriteFile (hPipe, Msg, lstrlen(Msg) &dwWritten, NULL ); wird "Quit" uebermittelt // Nachricht wird in // die Pipe geschrieben + 1, if( fWritten == FALSE || dwWritten == 0 ) { cout << "Fehler beim Schreiben" << endl ; break; } else cout << "Der Wert: " << Msg << " wurde gesendet" << endl; Sleep((rand() %1000)+500); zahl++; } CloseHandle(hPipe); ein = getch(); return 0; } //Handle der Pipe wird geschlossen Bildschirmausgabe der Prozesse ben. Pipe Empfänger und ben. Pipe Sender: 162 6 Beispielprogramme 6.3 Beispielprogramme für Unix / Linux 6.3.1 Counter Das Programm Counter ist ein einfacher Zähler, der die Zahlen 0 bis 10 auf dem Bildschirm ausgibt. Source-Code Counter (Dateiname Counter.c): #include<stdio.h> main() { int ein; int lauf,lauf2,hilf; /* Hilfsvariablen fuer Schleifen */ printf("\nZum Starten und Beenden bitte Eingabetaste druecken\n\n"); scanf("%c",&ein); for(hilf=0;hilf<=10;hilf++) { printf("Counter: %i\n",hilf); for (lauf=0;lauf<=100000;lauf++) for (lauf2=0;lauf2<=1000;lauf2++); } scanf("%c",&ein); return 0; } /* Einfacher Zaehler */ /* Schleife um den */ /* Prozessor zu belasten */ Bildschirmausgabe Counter: 163 6 Beispielprogramme 6.3.2 Prozess Das Programm Prozess ist, wie das Programm Counter, ein einfacher Zähler. Der Unterschied zwischen den beiden Programmen liegt darin, daß bei dem Programm Prozess der Programmcode für den Zähler als Sohnprozess programmiert wurde. Dadurch kann das Programm Prozess vom Anwender unterbrochen werden. Source-Code Prozess (Dateiname Prozess.c): #include<stdio.h> main() { int ein; int lauf,lauf2,hilf; int prozess_pid; /* Hilfsvariablen fuer Schleifen /* Variable um die PID des */ /* Sohnes zu speichern */ printf("\nZum Starten und Beenden bitte Eingabetaste druecken\n\n"); scanf("%c",&ein); */ if((prozess_pid = fork())==0) /* Sohnprozess wird erzeugt */ { for(hilf=0;hilf<=10;hilf++) /* Einfacher Zaehler */ { printf("Prozess: %i\n",hilf); for (lauf=0;lauf<=100000;lauf++) /* Schleife um das */ for (lauf2=0;lauf2<=1000;lauf2++); /* System zu belasten */ } exit(0); } scanf("%c",&ein); kill(prozess_pid,9); /* Sohnprozess wird terminiert, */ return 0; /* wenn er nicht bereits beendet ist */ } Bildschirmausgabe Prozess: 164 6 Beispielprogramme 6.3.3 Zwei Prozesse Bei diesem Programm wurden zwei Zähler als Prozesse programmiert. Beide Zähler laufen völlig unabhängig von einander. Source-Code ZweiProzesse (Dateiname 2Prozesse.c): #include<stdio.h> main() { int ein; int lauf,lauf2,hilf; /* Hilfsvariablen fuer Schleifen */ int prozess1_pid,prozess2_pid; /* Variable um die PID´s */ /* der Soehne zu speichern */ printf("\nZum Starten und Beenden bitte Eingabetaste druecken\n\n"); scanf("%c",&ein); if((prozess1_pid = fork())==0) /* 1. Sohnprozess wird erzeugt */ { for(hilf=0;hilf<=10;hilf++) /* Einfacher Zaehler */ { printf("Prozess 1: %i\n",hilf); for (lauf=0;lauf<=100000;lauf++) /* Schleife um das */ for (lauf2=0;lauf2<=1000;lauf2++); /* System zu belasten */ } exit(0); } if((prozess2_pid = fork())==0) /* 2. Sohnprozess wird erzeugt */ { for(hilf=0;hilf<=10;hilf++) /* Einfacher Zaehler */ { printf(" Prozess 2: %i\n",hilf); for (lauf=0;lauf<=100000;lauf++) /* Schleife um das */ for (lauf2=0;lauf2<=500;lauf2++); /* System zu belasten */ } exit(0); } scanf("%c",&ein); kill(prozess1_pid,9); /* Sohnprozesse werden terminiert */ kill(prozess2_pid,9); /* wenn sie nicht bereits beendet sind */ return 0; } 165 6 Beispielprogramme Bildschirmausgabe Zwei Prozesse: 166 6 Beispielprogramme 6.3.4 Semaphor Das Programm erzeugt einen Semaphor, der auf 3 initialisiert wird, und 10 Prozesse (Sohnprozesse). Alle 10 Prozesse versuchen nun den Semaphor zu dekrementieren, was den ersten 3 Prozesse auch gelingt. Alle anderen Prozesse müssen warten bis ein anderer Prozess den Semaphor inkrementiert. Bei der Arbeit mit einem Semaphor gibt es systemabhänige Unterschiede. Dieses Programm wurde für Linux und HP UNIX (Betriebssystem der Nebsy) programmiert. Vor dem Compelieren muß der entsprechende "define" Befehl gesetzt werden. Source-Code Semaphor (Dateiname Semaphor.c): /*#define _HPUX_SOURCE */ #define _Linux_SOURCE /* /* /* /* An diese Stelle kann festgelegt */ werden ob das Programm fuer */ Linux oder HP UNIX compiliert */ werden soll */ #include<stdio.h> #include<sys/ipc.h> #include<sys/sem.h> #include<stdlib.h> struct sembuf sem_p[1]; /* Strucktur fuer */ /* P-Operation auf Semaphor */ struct sembuf sem_v[1]; /* Strucktur fuer */ /* V-Operation auf Semaphor */ main() { int ein; int lauf,lauf2,hilf; int prozess_pid[10]; int anzahl; int semid; ushort initarray[1]; ushort outarray[1]; #ifdef _Linux_SOURCE union semun para; union semun para2; para.array=initarray; para2.array=outarray; #endif /* /* /* /* /* /* Feld um die PID´s der */ Sohn-Prozesse zu speichern */ Anzahl der Sohn-Prozesse */ ID der Semaphorengruppe */ Initialisierungsfeld */ Ausgabefeld */ /* Variablen zum Arbeiten unter Linux */ 167 6 Beispielprogramme initarray[0]=3; semid =semget(IPC_PRIVATE,1,IPC_CREAT|0777); /* Erzeugung einer Semaphorgruppe mit einem Semaphor */ #ifdef _Linux_SOURCE semctl(semid,0,SETALL,para); #else /* Setzen des Semaphores auf 3 */ semctl(semid,0,SETALL,initarray); #endif sem_p[0].sem_num = 0; sem_p[0].sem_op = -1; sem_p[0].sem_flg = 0; /* Vorbereitung der P-Operation */ sem_v[0].sem_num = 0; sem_v[0].sem_op = 1; sem_v[0].sem_flg = 0; /* Vorbereitung der V-Operation */ printf("\nZum Starten und Beenden bitte Eingabetaste druecken\n\n"); scanf("%c",&ein); for (anzahl=0;anzahl<10;anzahl++) /* 10 Sohn-Prozesse */ { /* werden erzeugt */ if((prozess_pid[anzahl]=fork())==0) { printf("Kunde %i betritt den Laden \n",anzahl); semop(semid,sem_p,1); /* Semaphor dekrementieren */ printf("Kunde %i wird bedient \n",anzahl); sleep(5); /* Prozess schlaeft 5 Sekunden */ printf("Kunde %i verlaesst den Laden\n",anzahl); semop(semid,sem_v,1); /* Semaphor inkrementieren */ exit(0); /* Prozess terminiert */ } } scanf("%c",&ein); for (anzahl=0;anzahl<10;anzahl++) /* terminiere die Sohn- */ kill(prozess_pid[anzahl],9); /* Prozesse wenn diese nicht */ /* bereits terminiert sind */ #ifdef _Linux_SOURCE semctl(semid,0,IPC_RMID,para); /* Loeschen der */ #else /* Semaphorengruppe in */ semctl(semid,0,IPC_RMID,initarray); /* Linux oder HP UNIX */ #endif return 0; } 168 6 Beispielprogramme Bildschirmausgabe Semaphor: 169 6 Beispielprogramme 6.3.5 Signale Bei diesem Programm wird die Reihenfolge, in der die beiden Sohnprozesse (Zähler) abgearbeitet werden, durch die Verwendung von Signalen gesteuert. Source-Code Signale (Dateiname Signale.c): #include<stdio.h> #include<signal.h> void sighand() { signal(SIGUSR1,&sighand); /* Signal Handler der beim */ /* Eintreffen eines bestimmten */ /* Signales ausgefuehrt wird */ /* Bindung des Signales SIGUSR1 */ /* an den Signal Handler sighand()*/ printf("Habe Signal SIGUSR1 erhalten\n"); } main() { int ein; int lauf,lauf2,hilf; int vater_pid,prozess1_pid,prozess2_pid; signal (SIGUSR1,&sighand); /* Bindung des Signales SIGUSR1 */ /* an den Signal Handler sighand() */ printf("\nZum Starten und Beenden bitte Eingabetaste druecken\n\n"); scanf("%c",&ein); if((prozess1_pid = fork())==0) { vater_pid=getppid(); /* 1. Sohnprozess wird erzeugt */ /* Sohnprozess erfragt */ /* die PID des Vaters */ for(hilf=0;hilf<=10;hilf++) /* Einfacher Zaehler */ { printf("Prozess 1: %i\n",hilf); for (lauf=0;lauf<=1000;lauf++) for (lauf2=0;lauf2<=10000;lauf2++); } kill(vater_pid,SIGUSR1); /* Dem Vaterprozess wird */ /* das Signal SIGUSR1 gesendet */ exit(0); } /* 1. Sohnprozess terminiert */ 170 6 Beispielprogramme if((prozess2_pid = fork())==0) { pause(); /* 2. Sohnprozess wird erzeugt */ /* 2. Sohnprozess wartet /* auf ein Signal */ */ for(hilf=0;hilf<=10;hilf++) /* Einfacher Zaehler */ { printf(" Prozess 2: %i\n",hilf); for(lauf=0;lauf<=1000;lauf++) for(lauf2=0;lauf2<=10000;lauf2++); } exit(0); /* 2. Sohnprozess terminiert */ } pause(); /* Vaterprozess wartet auf ein Signal */ kill(prozess2_pid,SIGUSR1); /* Vaterprozess sendet das Signal */ scanf("%c",&ein); /* SIGUSR1 an den 2. Sohnprozess */ return 0; } Bildschirmausgabe Signale: 171 6 Beispielprogramme 6.3.6 Unbenannte Pipe Die Erzeugung und Verwendung einer unbenannten Pipe zur Kommunikation zwischen zwei Prozessen, die eine Vater Sohn Beziehung zueinander haben, soll in diesem Programm möglichst einfach erläutert werden. Source-Code unbenannte Pipe (Dateiname unbPipe.c): #include<stdio.h> #include<stdlib.h> #include<string.h> main() { int ein; /* Hilfsvariable fuer Programmstart */ int hilf; int schreibprozess_pid,leseprozess_pid; char inbuffer[2]; /* Hilfsvariable zum Umformen */ char outbuffer[2]; /* und Uebermitteln der Daten */ int fd[2]; /* Dateideskriptoren fuer Leseende */ /* (fd[0]) und Schreibende (fd[1]) */ printf("\nZum Starten und Beenden bitte Eingabetaste druecken\n\n"); scanf("%c",&ein); pipe(fd); /* unbenannte Pipe wird erzeugt */ if((schreibprozess_pid = fork())==0) /* Schreibprozess wird */ { /* erzeugt */ for(hilf=0;hilf<10;hilf++) { close(fd[0]); /* Dateideskriptor fuer das */ /* Leseende wird geschlossen */ inbuffer[0]=48+hilf; write(fd[1],inbuffer,2); /* 2 Bytes werden in die */ /* Pipe geschrieben*/ printf("Schreibprozess: schreibe den Wert %c in die Pipe\n",inbuffer[0]); sleep(1); } exit(0); } 172 6 Beispielprogramme if((leseprozess_pid = fork())==0) /* Leseprozess wird erzeugt */ { close(fd[1]); /* Dateideskriptor fuer das Schreibende */ while(1) /* wird geschlossen */ { read(fd[0],outbuffer,2); /* 2 Bytes werden aus der */ /* Pipe ausgelesen */ printf(" Leseprozess: lese den Wert %s aus der Pipe\n",outbuffer); sleep(2); } exit(0); } scanf("%c",&ein); kill(schreibprozess_pid,9); /* Schreib- und Leseprozess */ kill(leseprozess_pid,9); /* werden terminiert wenn sie */ /* nicht bereits beendet sind */ return 0; } Bildschirmausgabe unbenannte Pipe: 173 6 Beispielprogramme 6.3.7 Benannte Pipe für zwei getrennte Prozesse In Unix / Linux können benannte Pipes auch für die Kommunikation zwischen Prozesse eingesetzt werden, die nicht miteinander "verwandt" sind. Dies soll durch die beiden Programme benanntePipeE und benanntePipeS gezeigt werden. Source-Code benannte Pipe Empfänger (Dateiname benPipeE.c): #include<stdio.h> #include<fcntl.h> main() { int ein; /* Hilfsvariable fuer Programmstart */ int hilf; char outbuffer[2]; /* Buffer zum Auslesen der Pipe */ int fd; /* Dateideskriptor fuer Pipe */ int lese; /* speichert die Anzahl der gelesenen Bytes */ printf("\nZum Starten bitte Eingabetaste druecken\n"); scanf("%c",&ein); printf("Empfaengerprozess wurde gestartet\n\n"); do { fd = open("MY_PIPE",O_RDONLY); /* Oeffnen der Pipe zum Lesen */ if (fd==-1) /* Dateideskriptor wird */ /* zurueckgegeben */ printf("Bitte Prozess zum Schreiben in die Pipe starten\n"); sleep(2); } while (fd==-1); while(1) /* Endlosschleife zum Auslesen der Pipe */ { lese=read(fd,outbuffer,2); /* 2 Bytes werden ausgelesen */ if (lese==0) break; printf("Leseprozess: lese den Wert %c aus der Pipe\n",outbuffer[0]); sleep(2); } unlink("MY_PIPE"); /* benannte Pipe wird geloescht */ return 0; } 174 6 Beispielprogramme Source-Code benannte Pipe Sender (Dateiname benPipeS.c): #include<stdio.h> #include<fcntl.h> main() { int ein; int hilf; char inbuffer[2]; int fd; /* Hilfsvariable fuer Programmstart */ /* Buffer zum Schreiben in die Pipe */ /* Dateideskriptor fuer Pipe */ printf("\nZum Starten bitte Eingabetaste druecken\n"); scanf("%c",&ein); printf("Sendeprozess wurde gestartet\n\n"); mkfifo("MY_PIPE",0666); /* benannte Pipe wird erzeugt */ fd = open("MY_PIPE",O_WRONLY); /* Oeffnen der Pipe zum */ /* Schreiben Dateideskriptor */ /* wird zurueckgegeben */ for(hilf=0;hilf<10;hilf++) { inbuffer[0]=48+hilf; write(fd,inbuffer,2); /* Zaehler zum Schreiben */ /* in die Pipe */ /* 2 Bytes werden in die */ /* Pipe geschrieben */ printf("Schreibprozess: schreibe den Wert %c in die Pipe\n",inbuffer[0]); sleep(1); } return 0; } 175 6 Beispielprogramme Bildschirmausgabe benannte Pipe für zwei getrennte Prozesse: Empfängerprozess: Sendeprozess: 176 7 Abschlussbetrachtung 7 Abschlussbetrachtung Diese Diplomarbeit ermöglicht einen Vergleich, wie die Steuerung, Synchronisation und Kommunikation von Prozessen / Threads in den Betriebssystemen Windows NT, Unix / Linux und der Programmiersprache Java implementiert ist. Die benötigten Funktionen bzw. Methoden präzise und vollständig zu erläutern ohne dabei zu sehr abzuschweifen ist der zentrale Punkt dieser Diplomarbeit. Grundkenntnisse von der jeweiligen Umgebung werden vorausgesetzt, da sonst der Umfang, um alle drei Umgebungen zu erläutern, den Rahmen einer einzelnen Diplomarbeit sprengen würde. Obwohl in dieser Diplomarbeit die Unterschiede der einzelnen Umgebungen zu sehen sind, ist es sehr schwer zu sagen, welche Umgebung für die Arbeit mit Prozessen / Threads am geeignetsten ist, da jede Umgebung ihre Vor-und Nachteile hat. Meine persönliche Erfahrung bei der Arbeit mit diesen Umgebungen war allerdings, daß die Auswahl an Fachbüchern für Unix / Linux bzw. Java sehr umfangreich ist. Bei der Arbeit mit Windows NT ist die Auswahl an unabhänigen Büchern bzw. Internetquellen sehr beschränkt. 177 8 Anhang 8 Anhang 8.1 Literaturverzeichnis Titel, Autor Java Programmierhandbuch Stefan Middendorf Reiner Singer UNIX System V.4 Jürgen Gulbins Karl Obermayr [MiSi1999] [GuOb1995] Jahr Verlag 1999 dpunkt.verlag 1995 Springer-Verlag [Herold1994] UNIX Grundlagen Helmut Herold 1994 ADDISON-WESLEY [Stevens1995] Programmierung in der Unix-Umgebung W.Richard Stevens 1994 ADDISON-WESLEY [Vogt2001] Betriebssysteme Carsten Vogt 2001 Spektrum Akademischer Verlag 2001 ADDISON-WESLEY 1997 Markt&Technik Buchverlag [Hansen] [HaWi1997] Windows-Programmierung mit C++ Henning Hansen Windows NT 4 Programmierung David Hamilton Mickey Williams 8.2 Internetquellen URL zuletzt verfügbar [www01] http://www.boku.ac.at/javaeinf November 2001 [www02] http://www.boku.ac.at/htmleinf November 2001 [www03] http://www.spotlight.de November 2001 [www04] http://www.sun.de November 2001 [www05] http://www.borland.com November 2001 [www06] http://www.borland.com/bcppbuilder/freecompiler/ November 2001 [www07] http://www.pdfzone.com November 2001 178 8 Anhang 8.3 Werkzeuge Werkzeug Anwendung Borland Turbo Debugger Version 5.5 C / C++ Compiler ( WindowsNT ) Borland C++ 5.0 C / C++ Enwicklungsumgebung Borland JBuilder3 Trail Edition Java - Entwicklungsumgebung GNU C / C++ Compiler C / C++ Compiler (SuSE Linux 6.1) SmartDraw 5 Zeichen- und Designsoftware Adobe Acrobat 4.0 Dokumentenverwaltung Microsoft Word 97 Textverarbeitung Anmerkung: Einige Beispielprogramme für Windows NT wurden von der Borland C++ Entwicklungumgebung nicht compiliert. Mit dem frei erhältlichen Turbo Debugger 5.5 [www06] werden alle Beispielprogramme für Windows NT ohne Fehlermeldung compiliert. Um die Arbeit mit diesem Compiler zu erleichtern habe ich eine Configurationsdatei mit einigen Parameter in das BinVerzeichnis des Turbo Debuggers eingefügt: bcc32.cfg -I"C:\BCC55\INCLUDE" -L"C:\BCC55\lib;C:\BCC55\lib\PSDK" -DWINVER=0x0400 -D_WIN32_WINNT=0x0400 // // // // Pfad fuer INCLUDE Dateien Pfad fuer lib Dateien Parameter um Windows NT Programm zu compilieren Mit dem Aufruf bcc32 c:\Source\xxx.cpp im Bin-Verzeichnis des Turbo Debuggers wird ohne weitere Parameter ein Programm für Windows NT compiliert . 179 8 Anhang 8.4 Source-Code des Beispiel Applets 8.4.1 Die Klasse Applet1.java package BeispielApplet; import import import import import import java.awt.*; java.awt.event.*; java.applet.*; javax.swing.*; java.beans.*; java.net.*; // Klasse zur Initialisierung der Oberflaeche // und Ausfuehrung der Befehle public class Applet1 extends Applet implements ActionListener,FocusListener{ boolean isStandalone = false; // Auswahlbox fuer Synchronisationsarten private CheckboxGroup myCheckboxGroup; private Checkbox groupBox1,groupBox2,groupBox3,groupBox4; private boolean sync=false; private private private private // Variable fuer Synchronisation // durch Ereignis boolean syncjoin = false; // Variable zur Synchr. auf // das Ende eines Threads JButton jButton_Counter; // startet den Counter JButton jButton_Thread2; // startet Thread 2 JButton jButton_Thread2stop; // stoppt Thread 2 JButton jButton_Thread1; // startet Thread 1 JButton jButton_Thread1stop; // stoppt Thread 1 JButton jButton_2Threads; // startet beide Threads JButton jButton_Threadsstop; // anhalten und // weiterfuehren aller Threads JButton jButton_RESET; // RESET Button JButton jButton_Thread3; // startet Thread 3 JButton jButton_Thread3stop; // stoppt Thread 3 ImageCanvas screen; private private private private CountingThread thread1; Runnable rthread,bthread; Thread thread2, thread3; int lauf = 0; private private private private private private private private // // // // Objekt fuer Thread 1 Runnable Objekte Objekte fuer Thread 2 u. 3 Laufvariable private int[] thindex = new int [3]; // Thread Index um // festzuhalten welcher Thread gestartet wurde private String sleepthread1; private String sleepthread2; // Variable um sleep-Zeit zu // speichern, wenn durch // Prioritaet synchronisiert wird 180 8 Anhang private Steuerung steuer = new Steuerung(false); // Steuerung zum Anhalten der Threads private Beenden stop2 = new Beenden(false); private Beenden stop3 = new Beenden(false); // stop2 ist ein Objekt der Klasse Beenden und dient dazu dem // Thread 2 anzuzeigen, das er seine run() Methode beenden soll // stop3 ist fuer Thread 3 private Syncprio syncprio = new Syncprio(false,1000); // Über diese Objekt dient Synchronisation durch Prioritaet private IntJTextField jTextFieldA[] = new IntJTextField[13]; // Feld fuer 13 Text Felder (Eingabefelder) wird erzeugt JLabel JLabel JLabel JLabel JLabel JLabel JLabel JLabel JLabel JLabel JLabel JLabel JLabel jLabel1 = new JLabel(); jLabel2 = new JLabel(); jLabel3 = new JLabel(); jLabel4 = new JLabel(); jLabel5 = new JLabel(); // Labels fuer Beschriftung jLabel7 = new JLabel(); // der Oberflaeche jLabel8 = new JLabel(); jLabel9 = new JLabel(); jLabel10 = new JLabel(); jLabel11 = new JLabel(); jLabel12 = new JLabel(); jLabel13 = new JLabel(); jLabel14 = new JLabel(); Box box1; Box box2; Box box3; JButton JButton JButton JButton JButton JButton JPanel JPanel JPanel JPanel JPanel JPanel // Boxen fuer Oberflaeche jButton1 jButton2 jButton3 jButton4 jButton5 jButton6 jPanel1 jPanel2 jPanel3 jPanel4 JPanel5 JPanel6 = = = = = = = = = = = = new new new new new new new new new new new new // Buttons um die Prioritaet zu aendern JButton(); // Thread 1 ab JButton(); // Thread 1 auf JButton(); // Thread 2 ab JButton(); // Thread 2 auf JButton(); // bel. Thread ab JButton(); // bel. Thread auf JPanel(); JPanel(); JPanel(); JPanel(); JPanel(); JPanel(); // Panels fuer Oberflaeche MeineTextArea textArea1 = new MeineTextArea(); GridLayout gridLayout1 = new GridLayout(); GridLayout gridLayout2 = new GridLayout(); // Ausgabefeld 181 8 Anhang // Parameterwert holen public String getParameter(String key, String def) { return isStandalone ? System.getProperty(key, def) : (getParameter(key) != null ? getParameter(key) : def); } // Das Applet konstruieren public Applet1() { } // Das Applet initialisieren public void init() { try { jbInit(); } catch(Exception e) { e.printStackTrace(); } } // Initialisierung der Komponente private void jbInit() throws Exception { for (int i=0; i<thindex.length; i++) thindex[i]=0; // Schleife um die Textfelder zu initialisieren. Es wird // festgelegt welchen Wertebereich die Textfelder haben. for(int i=0;i<=jTextFieldA.length-1;i++) { if (i==3||i==7||i==11) jTextFieldA[i] = new IntJTextField(1,10); else { if(i==12) jTextFieldA[i] = new IntJTextField(0,9999); else jTextFieldA[i] = new IntJTextField(0,100000); } } for (int i=0;i<=10;i++) // Textfelder werden dem Focus { // und Action Listener zugefuehrt jTextFieldA[i].addFocusListener(this); jTextFieldA[i].addActionListener(this); } jTextFieldA[0].setText("0"); jTextFieldA[1].setText("50"); jTextFieldA[2].setText("300"); jTextFieldA[3].setText("4"); // Werte fuer Textfelder jTextFieldA[4].setText("51"); // werden vorgegeben jTextFieldA[5].setText("100"); jTextFieldA[6].setText("400"); jTextFieldA[7].setText("4"); jTextFieldA[8].setText("0"); jTextFieldA[9].setText("20"); jTextFieldA[10].setText("200"); 182 8 Anhang // Textfeld fuer die Eingabe der Schleifenzahl wird dem jTextFieldA[11].setText("2"); // ActionListener zugefuehrt jTextFieldA[11].setBounds(new Rectangle(285, 25, 20, 25)); jTextFieldA[11].addActionListener(new Applet1_jTextFieldA11_actionAdapter(this)); // Textfeld fuer die Eingabe der Schleifenzahl wird dem jTextFieldA[12].setText("100"); // ActionListener zugefuehrt jTextFieldA[12].setBounds(new Rectangle(265, 50, 40, 25)); jTextFieldA[12].addActionListener(new Applet1_jTextFieldA12_actionAdapter(this)); box1 = Box.createVerticalBox(); box1.setBounds(new Rectangle(225, 35, 120, 100)); box1.add(jTextFieldA[0], null); box1.add(jTextFieldA[1], null); // Grafische Oberflaeche box1.add(jTextFieldA[2], null); // wird initalisiert box1.add(jTextFieldA[3], null); box2 = Box.createVerticalBox(); box2.setBounds(new Rectangle(350, 35, 120, 100)); box2.add(jTextFieldA[4], null); box2.add(jTextFieldA[5], null); // Grafische Oberflaeche box2.add(jTextFieldA[6], null); // wird initalisiert box2.add(jTextFieldA[7], null); box3 = Box.createVerticalBox(); box3.setBounds(new Rectangle(100, 35, 120, 75)); box3.add(jTextFieldA[8], null); box3.add(jTextFieldA[9], null); // Grafische Oberflaeche box3.add(jTextFieldA[10], null); // wird initalisiert // Buttons werden erzeugt und initialisiert jButton_Counter = new JButton("Counter start"); jButton_Counter.addActionListener(this); jButton_Counter.setBounds(new Rectangle(100, 140, 120, 25)); jButton_Thread2 = new JButton("Thread 2 start"); jButton_Thread2.addActionListener(this); jButton_Thread2.setBounds(new Rectangle(0, 5, 120, 25)); jButton_Thread1 = new JButton("Thread 1 start"); jButton_Thread1.addActionListener(this); jButton_Thread1.setBounds(new Rectangle(0, 5, 120, 25)); jButton_2Threads = new JButton("Thread 1 u. Thread 2 starten"); jButton_2Threads.addActionListener(this); jButton_2Threads.setBounds(new Rectangle(35, 137, 262, 27)); jButton_Threadsstop = new JButton("Alle Threads anhalten"); jButton_Threadsstop.addActionListener(this); jButton_Threadsstop.setBounds(new Rectangle(320, 460, 200, 25)); 183 8 Anhang jButton_RESET = new JButton("RESET"); jButton_RESET.addActionListener(this); jButton_RESET.setBounds(new Rectangle(525, 460, 100, 25)); jButton_Thread3 = new JButton("Thread 3 start"); jButton_Thread3.addActionListener(this); jButton_Thread3.setBounds(new Rectangle(0, 5, 120, 25)); jButton_Thread1stop=new JButton("Thread 1 stop"); jButton_Thread1stop.addActionListener(this); jButton_Thread1stop.setBounds(new Rectangle(0, 35, 120, 25)); jButton_Thread2stop=new JButton("Thread 2 stop"); jButton_Thread2stop.addActionListener(this); jButton_Thread2stop.setBounds(new Rectangle(0, 35, 120, 25)); jButton_Thread3stop=new JButton("Thread 3 stop"); jButton_Thread3stop.addActionListener(this); jButton_Thread3stop.setBounds(new Rectangle(0, 35, 120, 25)); jButton1.setBounds(new Rectangle(65, 95, 55, 25)); jButton1.setFont(new java.awt.Font("Dialog", 0, 10)); jButton1.setText("ab"); jButton1.addActionListener(new java.awt.event.ActionListener(){ public void actionPerformed(ActionEvent e){ jButton1_actionPerformed (e); }}); jButton2.setBounds(new Rectangle(65, 65, 55, 25)); jButton2.setFont(new java.awt.Font("Dialog", 0, 10)); jButton2.setText("auf"); jButton2.addActionListener(new java.awt.event.ActionListener(){ public void actionPerformed(ActionEvent e){ jButton2_actionPerformed (e); }}); jButton3.setBounds(new Rectangle(65, 95, 55, 25)); jButton3.setFont(new java.awt.Font("Dialog", 0, 10)); jButton3.setText("ab"); jButton3.addActionListener(new java.awt.event.ActionListener(){ public void actionPerformed(ActionEvent e){ jButton3_actionPerformed (e); }}); jButton4.setBounds(new Rectangle(65, 65, 55, 25)); jButton4.setFont(new java.awt.Font("Dialog", 0, 10)); jButton4.setText("auf"); jButton4.addActionListener(new java.awt.event.ActionListener(){ public void actionPerformed(ActionEvent e){ jButton4_actionPerformed (e); }}); 184 8 Anhang jButton5.setBounds(new Rectangle(65, 95, 55, 25)); jButton5.setFont(new java.awt.Font("Dialog", 0, 10)); jButton5.setText("ab"); jButton5.addActionListener(new java.awt.event.ActionListener(){ public void actionPerformed(ActionEvent e){ jButton5_actionPerformed (e); }}); jButton6.setBounds(new Rectangle(65, 65, 55, 25)); jButton6.setFont(new java.awt.Font("Dialog", 0, 10)); jButton6.setText("auf"); jButton6.addActionListener(new java.awt.event.ActionListener(){ public void actionPerformed(ActionEvent e){ jButton6_actionPerformed (e); }}); // Labels werden initialisiert jLabel1.setHorizontalAlignment(SwingConstants.RIGHT); jLabel1.setText("Priorität 1 - 10 :"); jLabel1.setBounds(new Rectangle(5, 110, 90, 25)); jLabel2.setHorizontalAlignment(SwingConstants.RIGHT); jLabel2.setText("von :"); jLabel2.setBounds(new Rectangle(5, 35, 90, 25)); jLabel3.setHorizontalAlignment(SwingConstants.RIGHT); jLabel3.setText("bis :"); jLabel3.setBounds(new Rectangle(5, 60, 90, 25)); jLabel4.setHorizontalAlignment(SwingConstants.RIGHT); jLabel4.setText("sleep / ms :"); jLabel4.setBounds(new Rectangle(5, 85, 90, 25)); jLabel5.setHorizontalAlignment(SwingConstants.CENTER); jLabel5.setText("Synchronisieren?"); jLabel5.setBounds(new Rectangle(35, 5, 233, 17)); jLabel5.setFont(new java.awt.Font("Dialog", 0, 14)); jLabel7.setHorizontalAlignment(SwingConstants.CENTER); jLabel7.setText("Thread 1"); jLabel8.setHorizontalAlignment(SwingConstants.CENTER); jLabel8.setText("Counter"); jLabel9.setHorizontalAlignment(SwingConstants.CENTER); jLabel9.setText("Thread 2"); jLabel10.setHorizontalAlignment(SwingConstants.CENTER); jLabel10.setText("Thread 3"); jLabel11.setHorizontalAlignment(SwingConstants.RIGHT); jLabel11.setText("Aktuelle Priorität :"); jLabel11.setBounds(new Rectangle(100, 202, 115, 35)); 185 8 Anhang jLabel12.setHorizontalAlignment(SwingConstants.CENTER); jLabel12.setText(""); jLabel12.setBounds(new Rectangle(5, 69, 50, 35)); jLabel12.setFont(new java.awt.Font("Dialog", 0, 14)); jLabel13.setHorizontalAlignment(SwingConstants.CENTER); jLabel13.setText(""); jLabel13.setBounds(new Rectangle(12, 69, 41, 34)); jLabel13.setFont(new java.awt.Font("Dialog", 0, 14)); jLabel14.setText(""); jLabel14.setBounds(new Rectangle(12, 69, 35, 34)); jLabel14.setHorizontalAlignment(SwingConstants.CENTER); jLabel14.setFont(new java.awt.Font("Dialog", 0, 14)); myCheckboxGroup = new CheckboxGroup(); groupBox1 = new Checkbox("ja, mit Ereignis Ausgaben 1-10 :",myCheckboxGroup,false); groupBox2 = new Checkbox("ja, mit Priorität Schleifen :",myCheckboxGroup,false); groupBox3 = new Checkbox("ja, auf das Ende von Thread 2",myCheckboxGroup,false); groupBox4 = new Checkbox("nein",myCheckboxGroup,true); groupBox1.setBounds(new groupBox2.setBounds(new groupBox3.setBounds(new groupBox4.setBounds(new Rectangle(6, Rectangle(6, Rectangle(6, Rectangle(6, 25, 265, 25)); 50, 250, 25)); 75, 233, 25)); 100, 233, 25)); groupBox1.addItemListener(new java.awt.event.ItemListener() { public void itemStateChanged(ItemEvent e) { groupBox1_itemStateChanged(e); } }); groupBox2.addItemListener(new java.awt.event.ItemListener() { public void itemStateChanged(ItemEvent e) { groupBox2_itemStateChanged(e); } }); groupBox3.addItemListener(new java.awt.event.ItemListener() { public void itemStateChanged(ItemEvent e) { groupBox3_itemStateChanged(e); } }); groupBox4.addItemListener(new java.awt.event.ItemListener() { public void itemStateChanged(ItemEvent e) { groupBox4_itemStateChanged(e); } }); 186 8 Anhang jPanel1.setLayout(gridLayout1); jPanel1.setBackground(SystemColor.menu); jPanel1.setBounds(new Rectangle(95, 5, 500, 30)); jPanel1.add(jLabel8, null); jPanel1.add(jLabel7, null); jPanel1.add(jLabel9, null); jPanel1.add(jLabel10, null); jPanel2.setLayout(null); jPanel2.setBackground(SystemColor.menu); jPanel2.setBounds(new Rectangle(225, 135, 120, 125)); jPanel2.add(jButton_Thread1, null); jPanel2.add(jLabel12, null); jPanel2.add(jButton2, null); jPanel2.add(jButton1, null); jPanel2.add(jButton_Thread1stop, null); jPanel3.setLayout(null); jPanel3.setBackground(SystemColor.menu); jPanel3.setBounds(new Rectangle(350, 135, 120, 125)); jPanel3.add(jLabel13, null); jPanel3.add(jButton4, null); jPanel3.add(jButton3, null); jPanel3.add(jButton_Thread2, null); jPanel3.add(jButton_Thread2stop, null); jPanel4.setLayout(gridLayout2); jPanel4.setBounds(new Rectangle(475, 35, 120, 100)); JPanel5.setLayout(null); JPanel5.setBackground(SystemColor.menu); JPanel5.setBounds(new Rectangle(475, 135, 120, 125)); JPanel5.add(jLabel14, null); JPanel5.add(jButton_Thread3, null); JPanel5.add(jButton_Thread3stop, null); JPanel5.add(jButton6, null); JPanel5.add(jButton5, null); JPanel6.setLayout(null); JPanel6.setBackground(SystemColor.menu); JPanel6.setBorder(BorderFactory.createLineBorder (Color.black)); JPanel6.setBounds(new Rectangle(310, 265, 320, 180)); JPanel6.add(jTextFieldA[11], null); JPanel6.add(jLabel5, null); JPanel6.add(groupBox1, null); JPanel6.add(groupBox2, null); JPanel6.add(groupBox3, null); JPanel6.add(groupBox4, null); JPanel6.add(jButton_2Threads, null); JPanel6.add(jTextFieldA[12], null); textArea1.setBackground(Color.white); textArea1.setBounds(new Rectangle(5, 265, 300, 230)); 187 8 Anhang String imagebase = "bilder/ms"; String imagetype = "jpg"; // Pfad fuer Bilder // Format der Bilder Image images[] = new Image[18]; // Feld fuer 18 Bilder // Medistracker wird erzeugt MediaTracker tracker= new MediaTracker(this); // Bilder werden dem Mediatracker uebermittelt for (int i=0;i<18;i++) { //images[i] = getImage(this.getCodeBase(), imagebase+(i+1)+"."+imagetype); // Dieser Befehl wird benoetigt, wenn das // Applet ueber das Internet ausgefuehrt wird. images[i] = Toolkit.getDefaultToolkit().getImage( "D:/Dipl/BeispielApplet/bilder/ms"+(i+1)+".jpg"); // Mit diesem Befehl kann das Applet // im Appletviewer ausgefuehrt werden. tracker.addImage(images[i],i); } for (int i=0;i<18;i++) { try{tracker.waitForID(i); } catch (InterruptedException e){} } // Objekt zur Bildausgabe screen = new ImageCanvas(images,steuer,stop3,syncprio); screen.setBackground(Color.white); jPanel4.add(screen, null); this.setBackground(SystemColor.menu); this.setLayout(null); this.add(box1, null); this.add(box2, null); this.add(box3, null); this.add(jPanel1, null); this.add(jPanel2, null); this.add(jButton_Counter, null); this.add(jLabel1, null); this.add(jLabel4, null); this.add(jLabel3, null); this.add(jLabel2, null); this.add(jPanel3, null); this.add(jPanel4, null); this.add(JPanel5, null); this.add(jLabel11, null); this.add(textArea1, null); this.add(jButton_Threadsstop, null); this.add(jButton_RESET, null); this.add(JPanel6, null); } 188 8 Anhang // Applet-Information holen public String getAppletInfo() { return "Applet-Information"; } // Parameter-Infos holen public String[][] getParameterInfo() { return null; } // Main-Methode public static void main(String[] args) { Applet1 applet = new Applet1(); applet.isStandalone = true; Frame frame = new Frame(); frame.setTitle("Applet-Bereich"); frame.add(applet, BorderLayout.CENTER); applet.init(); applet.start(); frame.setSize(640,530); Dimension d = Toolkit.getDefaultToolkit().getScreenSize(); frame.setLocation((d.width - frame.getSize().width) / 2, (d.height - frame.getSize().height) / 2); frame.setVisible(true); } // Abfrage welcher Knopf gedrueckt wurde public void actionPerformed(ActionEvent e) { String s = e.getActionCommand(); if (s.equals("Counter start")) { Counter count = new Counter(textArea1); if (test()) // Funktion die kontrolliert, ob alle Textfelder // in ihrem Wertebereich sind // Dem Zaehler werden Daten uebergeben und gestartet count.setcounter(jTextFieldA[8].getValue(), jTextFieldA[9].getValue(),jTextFieldA[10].getValue()); } else if (s.equals("Thread 1 start")) { thread1start(); // Funktion um Thread 1 zu erzeugen } else if (s.equals("Thread 1 stop")) { if(thindex[0]==1) thread1.meinstop(); // Thread 1 wird ueber die // Methode meinstop gestoppt jLabel12.setText(""); // Anzeige der Prioritaet } // wird zurueckgesetzt else 189 8 Anhang if (s.equals("Alle Threads anhalten")) { steuer.setanhalten(); // Steuerungsobjekt wird auf true gesetzt textArea1.put("Alle Threads angehalten\n"); jButton_Threadsstop.setLabel("Alle Threads weiterführen"); } else if (s.equals("Alle Threads weiterführen")) { steuer.weiter(); // Steuerungsobjekt wird auf false gesetzt textArea1.put("Alle Threads weiterführen\n"); jButton_Threadsstop.setLabel("Alle Threads anhalten"); } else if (s.equals("Thread 1 u. Thread 2 starten")) { setsync(); // Synchronisationswert wird uebermittelt thread1start(); // Methode um den ersten Thread zu erzeugen thread2start(); // Methode um den zweiten Thread zu erzeugen if(syncjoin) // Prüfung, ob eine Reihenfoge beim { // Abarbeiten der Threads vorliegt. thread2.start(); // starten von Thread 2 thread1.joinset(thread2); // Thread 1 wird so gestartet, } // dass er nach dem Ende von } // Thread 2 anfaengt. else if (s.equals("Thread 2 start")) { thread2start(); // Eigene Methode um Thread 2 zu erzeugen } else if (s.equals("Thread 2 stop")) { stop2.set(true); // Setzt das Objekt stop2 auf true jLabel13.setText(""); // Anzeige der Prioritaet } // wird zurueckgesetzt else if (s.equals("RESET")) { textArea1.replaceText("",0,textArea1.getCaretPosition()); jLabel12.setText(""); jLabel13.setText(""); // Anzeigen werden zurueckgesetzt jLabel14.setText(""); jButton_Threadsstop.setLabel("Alle Threads anhalten"); stop2.set(false); stop3.set(false); // Threads erhalten ueber ein Objekt steuer.weiter(); // die Nachricht, dass sie stoppen sollen if (thindex[0]!=0 && thread1.isAlive()) thread1.stop(); // Alle laufenden if (thindex[1]!=0 && thread2.isAlive()) // Threads werden ohne thread2.stop(); // das sie ihre run() if (thindex[2]!=0 && thread3.isAlive()) // Methode beenden thread3.stop(); // koennen, gestoppt. } 190 8 Anhang else if (s.equals("Thread 3 start")) { // Es wird kontrolliert, if (thindex[2]==1 && thread3.isAlive()) // ob Thread 3 { // bereits laeuft. textArea1.put("Thread 3 is Alive\n"); } else { thread3 = new Thread(screen); // Thread 3 wird erzeugt thindex[2]=1; stop3.set(false); // Objekt stop3 wird auf false gesetzt thread3.setPriority(1); // Prioritaet von Thread 3 auf 1 thread3.start(); // Thread 3 wird gestartet textArea1.put("Thread 3 gestartet\n"); String hilf2 = String.valueOf(thread3.getPriority()); jLabel14.setText(hilf2); // Prioritaet wird ausgegeben } } else if (s.equals("Thread 3 stop")) { if (thread3.isAlive()) { stop3.set(true); // Objekt zum Beenden von Thread 3 textArea1.put("Thread 3 gestoppt\n"); // wird auf true gesetzt jLabel14.setText(""); // Prioritaetsanzeige wird zurueckgesetzt } } } // Methode um Thread 1 zu erzeugen und ggf. zu starten void thread1start(){ if (thindex[0]==1 && thread1.isAlive()) { // Abfrage ob Thread 1 bereits laeuft textArea1.put("Thread 1 is Alive\n"); } else { // Erzeugung des Thread Objektes thread1 = new CountingThread(syncprio,textArea1, steuer,jLabel12); if (test()) // Methode, die kontrolliert, ob alle { // Textfelder in ihrem Wertebereich sind. if (syncprio.getsync()) // Abfrage ob ueber die Prioritaet { // synchronisiert werden soll. thread1.setcountingthread(jTextFieldA[0].getValue(), jTextFieldA[1].getValue(),1); } // Setzen der Daten fuer den Thread else { thread1.setcountingthread(jTextFieldA[0].getValue(), jTextFieldA[1].getValue(),jTextFieldA[2].getValue()); } // Setzen der Daten fuer den Thread 191 8 Anhang thread1.setPriority(jTextFieldA[3].getValue()); // Prioritaet des Threads wird gesetzt String hilf2 = String.valueOf(thread1.getPriority()) ; jLabel12.setText(hilf2); // Ausgabe der Prioritaet setsync(); // Synchronisation durch Ereignisse ? if (!syncjoin) // Wird keine Synchronisation der thread1.start(); // Reihenfolge verlangt wird thindex[0]=1; // Thread 1 gestartet. jButton_Threadsstop.setLabel("Alle Threads anhalten"); steuer.weiter(); // Objekt das anzeigt ob die Threads } // anhalten sollen wird auf false gesetzt } } // Methode um Thread 2 zu erzeugen und ggf. zu starten void thread2start(){ if (thindex[1] != 0 && thread2.isAlive()) { textArea1.put("Thread 2 is Alive\n"); } else if (test()) // Textfelder in ihrem Wertebereich ? { if (syncprio.getsync()) // Synchr.n ueber die Prioritaet ? { rthread = new CountingThreadRunnable(jLabel13,syncprio,stop2, steuer,textArea1,jTextFieldA[4].getValue(), jTextFieldA[5].getValue(),1); } // Runnable Objekt wird erzeugt else // und Daten werden uebergeben { rthread = new CountingThreadRunnable(jLabel13,syncprio,stop2, steuer,textArea1,jTextFieldA[4].getValue(), jTextFieldA[5].getValue(),jTextFieldA[6].getValue()); } thread2 = new Thread(rthread); // Thread 2 wird erzeugt und das Runnable Objekt uebergeben thread2.setPriority(jTextFieldA[7].getValue()); String hilf3 = String.valueOf(thread2.getPriority()) ; jLabel13.setText(hilf3); stop2.set(false); // stop2 wird auf false gesetzt setsync(); // Synchronisation durch Ereignisse ? if (!syncjoin) // Wird keine Synchronisation der thread2.start(); // Reihenfolge verlangt thindex[1]=1; // wird Thread 2 gestartet. jButton_Threadsstop.setLabel("Alle Threads anhalten"); steuer.weiter(); // Objekt das anzeigt ob die Threads } // anhalten sollen wird auf false gesetzt } 192 8 Anhang // Methode, die ausgefuehrt wird um die Prioritaet // von Thread 1 herabzusetzen. void jButton1_actionPerformed(ActionEvent e){ if (thindex[0] != 0 && thread1.isAlive()) if (thread1.getPriority()>1) { thread1.setPriority(thread1.getPriority()-1); // Prioritaet wird um 1 herabgesetzt String s = String.valueOf(thread1.getPriority()) ; jLabel12.setText(s); // Neue Prioritaet wird angezeigt textArea1.put("Thread 1 neue Priorität : "+s+"\n"); } } // Methode, die ausgefuehrt wird um die Prioritaet // von Thread 1 heraufzusetzen. void jButton2_actionPerformed(ActionEvent e){ if (thindex[0] != 0 && thread1.isAlive()) if (thread1.getPriority()<10) { thread1.setPriority(thread1.getPriority()+1); // Prioritaet wird um 1 erhoeht String s = String.valueOf(thread1.getPriority()) ; jLabel12.setText(s); // Neue Prioritaet wird angezeigt textArea1.put("Thread 1 neue Priorität : "+s+"\n"); } } // Methode, die ausgefuehrt wird um die Prioritaet // von Thread 2 herabzusetzen void jButton3_actionPerformed(ActionEvent e){ if (thindex[1] != 0 && thread2.isAlive()) if (thread2.getPriority()>1) { thread2.setPriority(thread2.getPriority()-1); // Prioritaet wird um 1 herabgesetzt String s = String.valueOf(thread2.getPriority()) ; jLabel13.setText(s); // Neue Prioritaet wird angezeigt textArea1.put("Thread 2 neue Priorität : "+s+"\n"); } } // Methode, die ausgefuehrt wird um die Prioritaet // von Thread 2 heraufzusetzen void jButton4_actionPerformed(ActionEvent e){ if (thindex[1] != 0 && thread2.isAlive()) if (thread2.getPriority()<10) { thread2.setPriority(thread2.getPriority()+1); // Prioritaet wird um 1 erhoeht String s = String.valueOf(thread2.getPriority()) ; jLabel13.setText(s); // Neue Prioritaet wird angezeigt textArea1.put("Thread 2 neue Priorität : "+s+"\n"); } } 193 8 Anhang // Methode, die ausgefuehrt wird um die Prioritaet // von Thread 3 herabzusetzen void jButton5_actionPerformed(ActionEvent e){ if (thindex[2] != 0 && thread3.isAlive()) if (thread3.getPriority()>1) { thread3.setPriority(thread3.getPriority()-1); // Prioritaet wird um 1 herabgesetzt String s = String.valueOf(thread3.getPriority()) ; jLabel14.setText(s); // Neue Prioritaet wird angezeigt textArea1.put("Thread 3 neue Priorität : "+s+"\n"); } } // Methode, die ausgefuehrt wird um die Prioritaet // von Thread 3 heraufzusetzen void jButton6_actionPerformed(ActionEvent e){ if (thindex[2] != 0 && thread3.isAlive()) if (thread3.getPriority()<10) { thread3.setPriority(thread3.getPriority()+1); // Prioritaet wird um 1 erhoeht String s = String.valueOf(thread3.getPriority()); jLabel14.setText(s); // Neue Prioritaet wird angezeigt textArea1.put("Thread 3 neue Priorität : "+s+"\n"); } } public void focusLost(FocusEvent e){} public void focusGained(FocusEvent e){ test(); // Textfelder in ihrem Wertebereich ? } // Methode, die kontrolliert, ob alle Textfelder // in ihrem Wertebereich sind public boolean test(){ for(int i=0;i<=jTextFieldA.length-1;i++) { if (i==2) i++; if (i==6) i++; if(jTextFieldA[i].isValid()==false) { jTextFieldA[i].requestFocus(); return false; } // Ist die Eingabe eines Textfeldes nicht } // gueltig so erhaelt dieses Textfeld den Focus // und es wird false zurueckgegeben. if(!syncprio.getsync()&&jTextFieldA[2].isValid()==false) { jTextFieldA[2].requestFocus(); return false; // Hier wird geprueft ob } // durch die Prioritaet // synchronisiert werden soll. 194 8 Anhang if(!syncprio.getsync()&&jTextFieldA[6].isValid()==false) { // Wird durch die Prioritaet jTextFieldA[6].requestFocus();// synchronisiert stehen in return false; // diesen Felder keine Zahlen. } if (jTextFieldA[1].getValue() < jTextFieldA[0].getValue()) { jTextFieldA[1].requestFocus(); jTextFieldA[1].setText("keine pos. Differenz"); jTextFieldA[1].selectAll(); return false; // Es wird kontrolliert ob eine positive } // Differenz vorliegt. if (jTextFieldA[9].getValue() < jTextFieldA[8].getValue()) { jTextFieldA[9].requestFocus(); jTextFieldA[9].setText("keine pos. Differenz"); jTextFieldA[9].selectAll(); return false; } if (jTextFieldA[5].getValue() < jTextFieldA[4].getValue()) { jTextFieldA[5].requestFocus(); jTextFieldA[5].setText("keine pos. Differenz"); jTextFieldA[5].selectAll(); return false; } return true; } // Diese Methode stellt fest ob die Ausgabe // synchronisiert werden soll. public void setsync(){ if(jTextFieldA[11].isValid()==false) { jTextFieldA[11].setText("2");; } // Uebergibt die Anzahl der Ausgaben else // durch die synchronisiert werden soll. textArea1.set(sync,jTextFieldA[11].getValue()); } // Methode wird ausgefuehrt, wenn // durch Ereignisse synchronisiert werden soll void groupBox1_itemStateChanged(ItemEvent e) { if(thindex[0]==1) // Wenn Thread 1 laeuft thread1.meinstop(); // wird dieser beendet. stop2.set(true); // Objekt fuer Thread 2 // wird auf beenden gesetzt if (syncprio.getsync()) { // Synchronisation durch Prioritaet ? jTextFieldA[2].setText(sleepthread1) ; // sleep-Zeiten jTextFieldA[6].setText(sleepthread2) ; // zuruecksetzen. } 195 8 Anhang sync = true; // Synchr. durch Ereignisse auf true syncjoin=false; // Synchr. auf das Ende von Thread 2 auf false syncprio.setsync(false); // Synchr.durch Prioritaet auf false setsync(); // Anzahl der Ausgaben ermitteln textArea1.put("Ausgabe wird auf "+jTextFieldA[11].getValue()+" synchronisiert\n"); } // Methode wir ausgefuehrt, wenn // durch die Prioritaet synchronisiert werden soll void groupBox2_itemStateChanged(ItemEvent e) { if(thindex[0]==1) // Wenn Thread 1 laeuft thread1.meinstop(); // wird dieser beendet. stop2.set(true); // Objekt fuer Thread 2 // wird auf beenden gesetzt if(jTextFieldA[12].isValid()==false) // Textfeld fuer die { // Schleifenzahl jTextFieldA[12].requestFocus(); // gueltig ? } else // Schleifenzahl wird ausgelesen und uebergeben. syncprio.setschleifen(jTextFieldA[12].getValue()); sync=false; // Synchr. durch Ereignisse auf false syncjoin=false; // Synchr. auf das Ende von Thread 2 auf false syncprio.setsync(true); // Synchr.durch Prioritaet auf true sleepthread1 = jTextFieldA[2].getText(); sleepthread2 = jTextFieldA[6].getText(); jTextFieldA[2].setText("Schleifenzahl"); jTextFieldA[6].setText("Schleifenzahl"); // Werte fuer die sleep-Zeit der Threads wird // gesichert und der Text "Schleifenzahl" // in die entsprechenden Felder eingesetzt. setsync(); textArea1.put("Synchronisation mit Priorität\n"); if (jTextFieldA[12].isValid()) { // Schleifenzahl wird ermittelt und ausgegeben syncprio.setschleifen(jTextFieldA[12].getValue()); textArea1.put("Anzahl der Schleifen: "+jTextFieldA[12].getValue()*500000+"\n"); } } // Methode wird ausgefuehrt, wenn // auf das Ende des zweiten Threads synchronisiert. void groupBox3_itemStateChanged(ItemEvent e) { if(thindex[0]==1) // Wenn Thread 1 laeuft thread1.meinstop(); // wird dieser beendet. stop2.set(true); // Objekt fuer Thread 2 wird // auf beenden gesetzt 196 8 Anhang if (syncprio.getsync()) { // Sleep-Zeiten neu eingetragen jTextFieldA[2].setText(sleepthread1) ; jTextFieldA[6].setText(sleepthread2) ; } sync=false; // Synchr. durch Ereignisse auf false syncprio.setsync(false); // Synchr. durch Prioritaet auf false syncjoin=true; // Synchr. auf das Ende von Thread 2 auf true setsync(); textArea1.put("Synchronisation auf Ende von Thread 2\n"); } // Methode wird ausgefuehrt, wenn // nicht synchronisiert werden soll. void groupBox4_itemStateChanged(ItemEvent e) { if(thindex[0]==1) // Wenn Thread 1 laeuft thread1.meinstop(); // wird dieser beendet. stop2.set(true); // Objekt fuer Thread 2 wird if (syncprio.getsync()) // auf beenden gesetzt. { jTextFieldA[2].setText( sleepthread1) ; jTextFieldA[6].setText( sleepthread2) ; } sync=false; // Synchr. durch Ereignisse auf false syncprio.setsync(false); // Synchr. durch Prioritaet auf false syncjoin=false; // Synchr. auf das Ende von Thread 2 auf false setsync(); textArea1.put("Keine Synchronisation\n"); } // Methode wird ausgefuehrt, wenn eine Eingabe // im Textfeld 11 erfolgte. Dieses Eingabefeld // bestimmt die Anzahl der Ausgaben. void jTextFieldA11_actionPerformed(ActionEvent e) { if (sync && jTextFieldA[11].isValid()) // Eingabe des Textfeldes wird ueberprueft. textArea1.put("Ausgabe wird auf "+jTextFieldA[11].getValue()+" synchronisiert\n"); setsync(); // Methode wird aufgerufen um ggf. Werte } // zur Synchronisation zu uebergeben. // Methode wird ausgefuehrt, wenn eine Eingabe // im Text Feld 11 erfolgte.Dieses Eingabefeld // bestimmt die Anzahl der Schleifen. void jTextFieldA12_actionPerformed(ActionEvent e) { if (syncprio.getsync() && jTextFieldA[12].isValid()) { syncprio.setschleifen(jTextFieldA[12].getValue()); textArea1.put("Anzahl der Schleifen: "+jTextFieldA[12].getValue()*500000+"\n"); } } } 197 8 Anhang // Klasse des action_Adapter fuer das Eingabefeld 11 class Applet1_jTextFieldA11_actionAdapter implements java.awt.event.ActionListener { Applet1 adaptee; Applet1_jTextFieldA11_actionAdapter(Applet1 adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.jTextFieldA11_actionPerformed(e); } } // Klasse des action_Adapter fuer das Eingabefeld 12 class Applet1_jTextFieldA12_actionAdapter implements java.awt.event.ActionListener { Applet1 adaptee; Applet1_jTextFieldA12_actionAdapter(Applet1 adaptee) { this.adaptee = adaptee; } public void actionPerformed(ActionEvent e) { adaptee.jTextFieldA12_actionPerformed(e); } } 8.4.2 Die Klasse Beenden.java package BeispielApplet; // Klasse um den Threads, die mit Hilfe des // Runnable-Objektes erzeugt wurden // mitzuteilen, ob sie sich beenden sollen. public class Beenden { private boolean stop; // Konstruktor: Beim Erzeugen des Objekts muß // der Anfangswert übergeben werden. public Beenden(boolean wert) { this.stop=wert ; } // Methode um den Wert des Objektes zu aendern. public void set(boolean wert){ this.stop=wert; } // Methode um den Wert des Objektes abzufragen public boolean get(){ return this.stop; } } 198 8 Anhang 8.4.3 Die Klasse Counter.java package BeispielApplet; // Klasse die einen Zaehler enthaelt public class Counter { private int start; private int finish; private long time; MeineTextArea textArea1; // // // // Startwert des Zaehlers Ende des Zaehlers Zeit, die der Zaehler zwischen den Zaehlerschritten "schlafen" soll // TextArea fuer die Ausgabe // Konstruktor: Beim Erzeugen wird die Text Area uebergeben, // auf die die Ausgabe erfolgen soll. public Counter(MeineTextArea textArea1) { this.textArea1 = textArea1; } // Daten fuer den Zaehler werden uebernommen // und der Zaehler wird ausgefuehrt. public void setcounter (int from, int to, long zeit){ this.start = from; this.finish = to; this.time = zeit; textArea1.put("Counter getstartet\n"); for (int i=start;i<=finish; i++) // Schleife fuer den Zaehler { try{Thread.sleep(time);} // Sleep-Aufruf catch (InterruptedException e) {} textArea1.put("Conter: "+i+"\n"); // Ausgabe des Zaehlerstandes } textArea1.put("Counter beendet\n"); } } 199 8 Anhang 8.4.4 Die Klasse CountingThread.java package BeispielApplet; import javax.swing.*; // Klasse eines Zaehlers, die von der // Klasse Thread abgeleitet wird. public class CountingThread extends Thread { private int start; private int finish; // Variablen wie bei der Klasse Counter private long time; private boolean stop = false; // Variable fuer eigene // "stop"-Methode des Threads MeineTextArea textArea1; Steuerung steuer; Syncprio syncprio; // Objekte, die spaeter uebergeben werden JLabel jLabel12; // Konstruktor: Beim Erzeugen werden Objekte // von anderen Klassen uebergeben. public CountingThread(Syncprio syncprio,MeineTextArea textArea1,Steuerung steuer,JLabel jLabel12){ this.textArea1 = textArea1; this.steuer = steuer; this.syncprio=syncprio; this.jLabel12=jLabel12; } // Modifizierte "stop"-Methode public void meinstop(){ stop = true; } // Methode um die Daten fuer den Zaehler zu uebernehmen. public void setcountingthread(int from, int to,long zeit){ this.start = from; this.finish = to; this.time = zeit; } // Modifizierte "start"-Methode. Sie dient zum Starten, // wenn auf das Ende eines andern Thread gewartet werden soll public void joinset(Thread thread){ try{thread.join();} catch (InterruptedException f) {} super.start(); } // run-Methode des Threads public void run(){ textArea1.put("Thread 1 gestartet \n"); for(int i=start; i<= finish; i++) // Zaehlerschleife des Threads { if (!stop) // Es wird geprueft ob der { // Thread beendet werden soll. this.yield(); steuer.anhalten(); // Es wird geprueft ob der // Thread anhalten soll. 200 8 Anhang // Wenn die Threads durch Prioritaet synchronisiert // werden sollen, wird die Anzahl der Methodenaufrufe if (syncprio.getsync()) // ermittelt { for (int k=0;k<syncprio.getschleifen();k++) syncprio.zeit(); } else // Erfolgt keine Synchronisation durch Prioritaet { // wird die sleep-Methode aufgerufen. try{Thread.sleep(time);} catch (InterruptedException e) {} } textArea1.put1("Thread 1: "+i+"\n"); } // Ausgabe des Zaehlerstandes } if(stop) textArea1.put("Thread 1 gestoppt\n"); // Ausgabe wenn der else // Thread gestoppt wird. textArea1.put("Thread 1 beendet\n"); jLabel12.setText(""); textArea1.set(false,1); // Die letzten beiden Methoden werden textArea1.put1(""); // ausgefuehrt um eine eventuelle } // Synchronisation durch } // Ereignisse zu beenden. 8.4.5 Die Klasse CountingThreadRunnable.java package BeispielApplet; import javax.swing.*; // Klasse eines Zaehlers, mit dem Interface Runnable public class CountingThreadRunnable implements Runnable { private int start; private int finish; private long time; // Variablen wie bei der Klasse Counter MeineTextArea textArea1; Steuerung steuer; Beenden stop; // Objekte die spaeter uebergeben werden Syncprio syncprio; JLabel jLabel13; // Konstruktor: Beim Erzeugen werden Objekte von anderen // Klassen und die Daten fuer den Zaehler uebernommen public CountingThreadRunnable(JLabel jLabel13,Syncprio syncprio, Beenden stop2,Steuerung steuer, MeineTextArea textArea1,int from, int to,long zeit) { this.textArea1=textArea1; this.start = from; this.finish = to; this.time = zeit; this.steuer = steuer ; this.stop = stop2; this.syncprio=syncprio; this.jLabel13=jLabel13; } 201 8 Anhang // run-Methode des Interfaces Runnable public void run(){ textArea1.put("Thread 2 gestartet\n"); for (int i=start;i<=finish; i++) // Zaehlerschleife { steuer.anhalten(); // Soll angehalten werden ? if(stop.get()) // Soll gestoppt werden ? break; // Wenn die Threads durch Prioritaet synchronisiert // werden sollen wird die Anzahl der Methoden-Aufrufe if(syncprio.getsync()) // ermittelt. { for (int k=0;k<syncprio.getschleifen();k++) syncprio.zeit(); } // Erfolgt keine Synchronisation durch Prioritaet else // wird die sleep-Methode aufgerufen. try{Thread.sleep(time);} catch (InterruptedException e) {} textArea1.put2("Thread 2: "+i+"\n"); } // Ausgabe des Zaehlerstandes if (stop.get()) // Ausgabe, wenn der textArea1.put("Thread 2 gestoppt\n"); // Thread gestoppt wird. else textArea1.put("Thread 2 beendet\n"); jLabel13.setText(""); textArea1.set(false,1); // Die letzten beiden Methoden textArea1.put2(""); // werden ausgefuehrt um eine } // eventuelle Synchronisation durch } // Ereignisse zu beenden. 202 8 Anhang 8.4.6 Die Klasse ImageCanvas.java package BeispielApplet; import import import import java.awt.Canvas; java.applet.Applet; java.awt.*; java.awt.Image; // Klasse ImageCanvas mit dem Interface Runnable public class ImageCanvas extends Canvas implements Runnable{ private Image images[]; private int currentimage = 0; Steuerung steuer; Beenden stop; Syncprio syncprio; // Feld fuer die Bilder, // die ausgegeben werden sollen. // Variable fuer Bild // Objekte die spaeter uebergeben werden // Konstruktor: Hier wird ein Feld, das die Bilder enthaelt // und Objekte anderer Klassen uebergeben. public ImageCanvas(Image images[],Steuerung steuer, Beenden stop3,Syncprio syncprio) { this.images = images; this.steuer = steuer; this.stop = stop3; this.syncprio = syncprio; } // Methode zum Zeichnen des Bildes public void paint(Graphics g){ update(g); } // Methode zum Zeichnen des Bildes public void update(Graphics g){ g.drawImage(images[currentimage],0,0,120,100,this); } // run-Methode des Interfaces Runnable public void run(){ while(true) { if (stop.get()) // Soll gestoppt werden ? break; steuer.anhalten(); // Soll angehalten werden ? repaint(); // Zeichnung des Bildes for (int k=1;k<5000;k++) // Schleife mit der das System syncprio.zeit(); // belastet und die // Bildwiederholrate gesteuert wird. } currentimage = (currentimage + 1) % images.length; // Variable fuer Bild wird geaendert } } 203 8 Anhang 8.4.7 Die Klasse IntJTextField.java package BeispielApplet; import javax.swing.JTextField; // Klasse wurde von JTextField abgeleitet und einige // Methoden ergaenzt bzw. der Konstruktor veraendert. public class IntJTextField extends JTextField { private int low; private int high; // untere Grenze des Wertebereiches // obere Grenze des Wertebereiches // Konstruktor der Klasse dem beim Erzeugen des Objektes // wird der Wertebereich der Eingabe uebergeben. public IntJTextField ( int min, int max){ this.low = min; this.high = max; } // Methode um festzustellen, ob die Eingabe des Objektes // ein Int-Wert im erlaubten Wertebereich ist. public boolean isValid(){ int value; try { // Eingabe wird in einen Int-Wert umgewandelt. value = Integer.parseInt(this.getText()) ; if (value < low || value > high) { // Liegt der eingegebene throw new NumberFormatException(); // Wert außerhalb des } // Wertebereiches } // erfolgt Fehlermeldung catch (NumberFormatException f) { // Es wird kontrolliert ob der Fehler durch // die fruehere Ausgabe "k.p.D." entstanden ist. if (this.getText().equals("keine pos. Differenz")) return false; this.setText(" "+this.low+ "-" +this.high+" eingeben"); this.selectAll(); // Bei einem Fehler this.requestFocus(); // wird der Wertebereich return false; // angezeigt und das betreffende } // Textfeld markiert. return true; } // Methode zum Auslesen des Textfeldes public int getValue(){ return Integer.parseInt(this.getText()); } } 204 8 Anhang 8.4.8 Die Klasse MeineTextArea.java package BeispielApplet; import java.awt.TextArea; import java.awt.Color; // Klasse wurde von TextArea abgeleitet // und einige Methoden ergaenzt public class MeineTextArea extends TextArea { private private private boolean String buffer; int soll; int anzahl=0; th1 = true; boolean sync = false; // // // // // // Anzahl der soll Ausgaben Anzahl der ist Ausgaben Variable die bei einer Synchr. angibt welcher Thread den Monitor erhaelt Variable die angibt ob eine Synchr. durch Ereignisse stattfinden soll. // Konstruktor public MeineTextArea() { } // Methode um die Synchronisation durch // Ereignisse zu starten und beenden public void set(boolean syncronisieren,int anzahl){ this.sync=syncronisieren; // Soll synchronisiert werden wird this.soll=anzahl; // die soll Anzahl uebergeben } // Synchronisierte Methode fuer die Ausgabe von Thread 1 public synchronized void put1 (String ausgabe){ if (sync) // Stellt fest, ob synchronisiert werden soll { if (!th1) // Wenn th1 = false try {wait();} // muß Thread 1 warten. catch(InterruptedException e) {} this.buffer=ausgabe; this.append(buffer); // Ausgabe des uebergebenen Strings anzahl=anzahl+1; if (anzahl>=soll) { anzahl=0; th1=false; // Variable fuer den Monitor wird umgesetzt notify(); // Eventuell wartender Thread erhaelt die } // Benachrichtigung, daß er weiter arbeiten darf. } else // Soll keine synchronisierte { // Ausgabe mehr stattfinden weil z.B. this.buffer=ausgabe; // nur noch ein Thread laeuft this.append(buffer); // wird diese Schleife abgearbeitet. notify(); } } 205 8 Anhang // Synchronisierte Methode fuer die Ausgabe von Thread 2 public synchronized void put2 (String ausgabe){ if (sync) // Stellt fest, ob synchronisiert werden soll. { if (th1) // Wenn th1 = true try {wait();} // muß Thread 2 warten. catch(InterruptedException e) {} this.buffer=ausgabe; this.append(" "+buffer); // Ausgabe des Strings anzahl=anzahl+1; // Die Anzahl wird heraufgesetzt. if (anzahl>=soll) // Vergleich von soll und ist Wert { anzahl=0; th1=true; // Variable fuer den Monitor wird umgesetzt notify(); // Eventuell wartender Thread erhaelt die } // Benachrichtigung, daß er weiter arbeiten darf. } else // Soll keine synchronisierte { // Ausgabe mehr stattfinden weil z.B. this.buffer=ausgabe; // nur noch ein Thread laeuft wird if (!buffer.equals("")) // diese Schleife abgearbeitet. this.append(" "+buffer); notify(); } } // Synchronisierte Methode fuer die Ausgabe // von allgemeinen Informationen. public synchronized void put (String ausgabe){ this.buffer=ausgabe; this.append(" "+buffer); } } 206 8 Anhang 8.4.9 Die Klasse Steuerung.java package BeispielApplet; // Diese Klasse wird verwendet um die Threads mit Hilfe // eines Objektes dieser Klasse // anzuhalten und wenn gewollt wieder weiter zu fuehren. public class Steuerung { private boolean halt; // Konstruktor dem beim Erzeugen eines Objektes // der Anfangswert uebergeben werden muß public Steuerung(boolean wert) { this.halt=wert; } // Methode um der Wert des Objektes auf true zu setzen public void setanhalten (){ this.halt=true; } // Methode bei dessen Ausfuehrung ein Thread // angehalten wird, wenn das "Steuerugsobjekt" true ist. public synchronized void anhalten (){ if (this.halt) { try {wait();} catch(InterruptedException e) {} } } // Methode um den Wert des Objektes auf false zu setzen. public synchronized void weiter(){ this.halt=false; notifyAll(); // Eventuell wartende Threads werden } // benachrichtigt, daß sie weiter } // ausgefuehrt werden koennen. 207 8 Anhang 8.4.10 Die Klasse Syncprio.java package BeispielApplet; // Klasse die fuer eine Synchronisation // durch Prioritaet benoetigt wird. public class Syncprio { private boolean sync; private int schleifen; // Konstruktor Die Schleifenzahl und ob // eine Synchronisation durch die Prioritaet erfolgen // soll wird beim Erzeugen festgelegt. public Syncprio(boolean wert, int schleifen) { this.schleifen=schleifen; this.sync=wert; } // Methode mit der die Schleifenzahl veraendert werden kann public void setschleifen(int schleifen){ this.schleifen=schleifen*500; } // Methode ueber die festgelegt wird ob eine Synchronisation // durch die Prioritaet erfolgen soll oder nicht public void setsync(boolean wert){ this.sync=wert; } // Methode ueber die die Schleifenzahl erfragt werden kann public int getschleifen(){ return this.schleifen; } // Methode ueber die erfragt werden kann ob eine // Synchronisation durch die Prioritaet erfolgen soll public boolean getsync(){ return this.sync; } // Methode um bei einer Synchronisation // durch die Prioritaet das System zu belasten public synchronized int zeit(){ int k=0; for (int i=0;i<=1000;i++) k=k+1; return k; } } 208 8 Anhang 8.4.11 Implementierung des Beispiel Applets in HTML-Seite Durch diese Implementierung beginnt ein Browser, der nicht Java fähig ist auf Wunsch des Anwenders selbstständig mit dem Download des benötigten Plug-In. Source-Code der HTML-Datei: <HTML><HEAD> <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=windows-1252"> <TITLE> HTML-Testseite </TITLE></HEAD><BODY> Das BeispielApplet erscheint in einem Java-fähigen Browser.<BR> <!--"CONVERTED_APPLET"--> <!-- CONVERTER VERSION 1.3 --> <OBJECT classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93" WIDTH = 635 HEIGHT = 500 NAME = "TestApplet" ALIGN = middle VSPACE = 0 HSPACE = 0 codebase="http://java.sun.com/products/plugin/1.3/jinstall-13win32.cab#Version=1,3,0,0"> <PARAM NAME = CODE VALUE = "BeispielApplet.Applet1.class" > <PARAM NAME = CODEBASE VALUE = "." > <PARAM NAME = NAME VALUE = "TestApplet" > <PARAM NAME="type" VALUE="application/x-java-applet;version=1.3"> <PARAM NAME="scriptable" VALUE="false"> <COMMENT> <EMBED type="application/x-java-applet;version=1.3" CODE = "BeispielApplet.Applet1.class" CODEBASE = "." NAME = "TestApplet" WIDTH = 635 HEIGHT = 500 ALIGN = middle VSPACE = 0 HSPACE = 0 scriptable=false pluginspage="http://java.sun.com/products/plugin/1.3/plugininstall.html"><NOEMBED></COMMENT> </NOEMBED></EMBED></OBJECT> <!-<APPLET CODE = "BeispielApplet.Applet1.class" CODEBASE = "." WIDTH = 635 HEIGHT = 500 NAME = "TestApplet" ALIGN = middle VSPACE = 0 HSPACE = 0> </APPLET> --> <!--"END_CONVERTED_APPLET"--> </BODY></HTML> 209