Saalübung Informatik II SS 2006 Threads, Monitore und Semaphore Virtuelle Speicherverwaltung Prozesse und Threads z Wünschenswert, Parallelität innerhalb eines einzigen Programms zur Verfügung zu haben und nicht nur innerhalb des Betriebssystems z Java ist eine Sprache, die nebenläufige Programmierung direkt unterstützt z Konzept beruht auf so genannten Threads z Parallel ablaufende Aktivitäten, die sehr schnell in der Umschaltung sind z Innerhalb eines Prozesses kann es mehrere Threads geben – – Threads laufen alle zusammen in dem selben Adressraum ab Threads eines Prozesses können daher untereinander auf ihre öffentlichen Daten zugreifen → mitunter problematisch! Prozesse und Threads z Moderne Betriebssysteme - Illusion, dass verschiedene Programme gleichzeitig ausgeführt werden z Quasiparallelität ~ Nebenläufigkeit z Nebenläufigkeit der Programme wird durch das Betriebssystem gewährleistet z die Prozesse werden alle paar Millisekunden umgeschaltet - verzahnte Bearbeitung der Prozesse z Scheduler - Teil des Betriebssystems, der die Prozessumschaltung übernimmt Threads erzeugen z Für nebenläufige Programme in Java ist die Klasse Thread zuständig z Die gewünschte, parallel auszuführende Aktivität wird in der Objektmethode run() programmiert z Java bietet zur Programmierung von Threads zwei Möglichkeiten an (1) Threads über die Schnittstelle Runnable implementieren (2) Die Klasse Thread erweitern Threads erzeugen Vorteile Threads erzeugen z Ableiten von Thread Wir bilden eine Unterklasse von Thread Für ein Programmstück, das nebenläufig ablaufen soll, überschreiben wir die run()-Methode Damit der Thread und damit das Programm startet, rufen wir die Objektmethode start() auf Nachteile Ableiten von Thread (extends Thread) Programmcode in run() kann die Methoden der Klasse Thread nutzen Da es in Java keine Mehrfachvererbung gibt, kann die Klasse nur Thread erweitern 1) Implementieren von Runnable (implements Runnable) Die Klasse kann von einer anderen, problemspezifischen Klasse erben Thread-Methoden können nur über Umwege genutzt werden 3) Threads erzeugen z Ableiten von Thread class DateThread extends Thread { public void run() { for ( int i = 0; i < 20; i++ ) System.out.println( new Date() ); } } Thread t = new DateThread(); t.start(); oder new DateThread().start(); 2) Zustände eines Threads z Bei einem Thread-Exemplar können wir einige Zustände feststellen: – – – – – noch nicht erzeugt, laufend (vom Scheduler berücksichtigt), nicht laufend (vom Scheduler nicht berücksichtigt), wartend, beendet Ende eines Threads z Allgemein ist ein Thread beendet, wenn eine der folgenden Bedingungen zutrifft: – – – die run()-Methode wurde ohne Fehler beendet in der run()-Methode tritt eine Exception auf, die die Methode beendet der Thread wurde von außen abgebrochen. Dazu kann die prinzipbedingt problematische Methode stop() verwendet werden – oder aber ein Flag gesetzt werden, dass die „Endlosschleife“ abbricht Notwendigkeit: Synchronisation z Problem: Mehrere Programme versuchen gleichzeitig auf eine Ressource zuzugreifen (z.B. Programmsegment, Drucker, …) z Lösung: Zugriff auf Ressource wird synchronisiert → gegenseitiger Ausschluss public void run { ... while (Flag) { ... } ... } – die virtuelle Maschine wird beendet – Folge: alle Threads werden ebenfalls beendet Sicherheitseigenschaft z z Wenn mehr Threads oder Prozesse den selben kritischen Bereich betreten, als in diesem Bereich zugelassen sind, ist die Sicherheitseigenschaft verletzt Die Erfüllung der Sicherheitseigenschaft bezeichnet man auch als partielle Korrektheit Synchronisation über kritische Abschnitte class T extends Thread { static int result; public void run() { ... } result ist somit in jeder Instanz von T sichtbar } Kritische Abschnitte Zusammenhängende Programmblöcke, die nicht unterbrochen werden dürfen und besonders geschützt werden müssen, nennen sich kritische Abschnitte. Wenn lediglich ein Thread den Programmteil abarbeitet, dann nennen wir dies gegenseitigen Ausschluss oder atomar. Kritische Abschnitte z Punkte parallel initialisieren z Nehmen wir an, ein Thread T1 belegt das vorher mit (0,0) belegte Point-Objekt p mit den Werten (1,2) und ein Thread T2 gleichzeitig mit den Werten (2,1). Das heißt, T1 führt die Anweisungen p.x = 1; p.y = 2; durch und T2 die Anweisungen p.x = 2; p.y = 1; z Führt im Einzelfall zu den folgenden möglichen Ergebnissen: { (1,2),(1,1,),(2,1),(2,2) } Abschnitte mit synchronized schützen z z Soll ein Programmabschnitt oder eine Objekt- oder Klassenmethode atomar ablaufen, so existiert dafür in Java ein Sprachkonstrukt. Wir setzen vor die Methode das Schlüsselwort synchronized Das oben genannte Problem mit foo() lässt sich mit synchronized einfach lösen: synchronized void foo() { i++; } Kritische Abschnitte public class IPlusPlus { static int i; static void foo() { i++; } } 0 getstatic #19 <Field int i> 3 iconst_1 4 iadd 5 putstatic #19 <Field int i> 8 return Die Objektmethode foo() erhöht die statische Variable i. Um zu erkennen, dass i++ ein kritischer Abschnitt ist, sehen wir uns den dazu generierten Bytecode für die Methode foo() an Monitore z Laufzeitumgebung lässt nur einen Thread in einen Block z Wie macht die Java-Maschine dies? z Jedes Java-Objekt definiert einen so genannten Monitor Der Begriff geht auf C. A. R. Hoare zurück, der im Aufsatz »Communicating Sequential Processes« von 1978 erstmals dieses Konzept veröffentlichte z Mit dem Monitor – wir nennen ihn auch Lock (leicht zu merken durch einen Schlüssel, der die Tür abschließt) – ist der serielle Zugriff gewährleistet z Wir setzen dann vor die Methode das Schlüsselwort synchronized z Ein betretender Thread setzt dann diesen binären Monitor des aufrufenden Objekts, und andere ankommende Threads müssen warten, wenn dieser gesetzt ist Deadlocks z z Deadlocks können in Java-Programmen nicht erkannt und verhindert werden Dem Programmierer fällt also die Aufgabe zu, diesen ungünstigen Zustand gar nicht erst herbeizuführen Deadlocks z Ein Deadlock kann nur dann vorliegen, wenn alle 4 folgenden Bedingungen gleichzeitig erfüllt sind: – Zyklische Wartebedingung (circular-wait) z – Wechselseitiger Ausschluss (exclusive-use) z – jedes Betriebsmittel eines Betriebssystems ist zu einem Zeitpunkt entweder nur von genau einem einzigen Prozess exklusiv belegt oder aber frei Belegungs- und Wartebedingung (hold-and-wait) z – ein Prozess wartet auf eine Ressource, die der nächste Prozess einer Prozesskette bereits zugewiesen bekommen hat (siehe Abb. der vorherigen Folie) alle am Deadlock beteiligten Prozesse belegen bereits Betriebsmittel und fordern noch weitere Betriebsmittel an, die momentan nicht frei sind Nicht-Unterbrechbarkeit (no-preemption) z ein Prozess kann einem anderen Prozess nicht einfach ein Betriebsmittel entziehen, das dieser zur Zeit belegt http://olli.informatik.uni-oldenburg.de/Deadlock/Html/Deadlock_Verfahren.html Synchronisation über Warten und Benachrichtigen z Die Synchronisation von Methoden oder Blöcken ist eine einfache Möglichkeit, konkurrierende Zugriffe von der virtuellen Maschine auflösen zu lassen z Obwohl die Umsetzung mit den Locks die Programmierung einfach macht, reicht dies für viele Aufgabenstellungen nicht aus z Zwar können Daten ausgetauscht werden, doch gewünscht ist dieser Austausch in einer synchronisierten Abfolge ¾ Threads sollen sich ggf. gegenseitig über den aktuellen Zustand informieren Synchronisation über Warten und Benachrichtigen z In Java wird dies durch die speziellen Objektmethoden wait() und notify() realisiert z Wenn wir den Lock am Objekt o festmachen und warten, dann schreiben wir: synchronized( o ) { try { o.wait(); } catch ( InterruptedException e ) {} } z Ein wait() kann mit einer InterruptedException vorzeitig abbrechen, wenn der wartende Thread per interrupt()-Methode unterbrochen wird Synchronisation über Warten und Benachrichtigen z z Wenn der aktuelle Thread das Lock eines Objekts besitzt, kann er Threads aufwecken, die auf dieses Objekt warten: Synchronisation über Warten und Benachrichtigen import java.util.LinkedList; import java.util.Queue; class Erzeuger extends Thread { private final static int MAXQUEUE = 13; private Queue<String> nachrichten = new LinkedList<String>(); synchronized void benachrichtige() { notifyAll(); } public void run() { try { while ( true ) { erzeuge(); sleep( (int)(Math.random()*1000) ); } } catch ( InterruptedException e ) { } } Um notify() muss es keinen eine Exception auffangenden Block geben public synchronized void erzeuge() throws InterruptedException { while ( nachrichten.size() == MAXQUEUE ) wait(); nachrichten.add( new java.util.Date().toString() ); notifyAll(); // oder notify(); } // vom Verbraucher aufgerufen public synchronized String verbrauche() throws InterruptedException { while ( nachrichten.size() == 0 ) wait(); notifyAll(); return nachrichten.poll(); } } Semaphore z Synchronisationsprobleme lassen sich durch kritische Abschnitte und Wartesituationen mit wait() und notify() lösen z Dennoch ist der eingebaute Mechanismus auch mit Nachteilen verbunden – eine große Schwierigkeit ist es, synchronisierende Programme zu entwickeln und zu warten – Synchronisationsvariablen verstreuen sich mitunter über große Programmteile und machen die Wartung schwierig – Teile, die bewusst atomar ausgeführt werden müssen, benötigen zwingend einen Programmblock und eine Synchronisationsvariable class Verbraucher extends Thread { Erzeuger erzeuger; String name; Verbraucher( String name, Erzeuger erzeuger ) { this.erzeuger = erzeuger; this.name = name; } public void run() { try { while ( true ) { String info = erzeuger.verbrauche(); System.out.println( name +" holt Nachricht: "+ info ); Thread.sleep( (int)(Math.random()*1000) ); } } catch ( InterruptedException e ) { } } } public class ErzeugerVerbraucherDemo { public static void main( String args[] ) { Erzeuger erzeuger = new Erzeuger(); erzeuger.start(); new Verbraucher( "Eins", erzeuger ).start(); new Verbraucher( "Zwei", erzeuger ).start(); new Verbraucher( "Drei", erzeuger ).start(); } } Semaphore z Eine Semaphore kann als eine Datenstruktur aufgefasst werden, die eine nicht-negative Integervariable und eine Referenz auf eine Warteschlange enthält z Auf einer Semaphoren s werden zwei Operationen P(s) und V(s) definiert z Anhand von s wird die Zuordnung einer Semaphore zu einer Warteschlange vorgenommen z Eine Semaphore wird als fair bezeichnet, wenn sie sicherstellt, dass kein wartender Prozess/Thread verhungern kann (Starvation) Semaphore z z Semaphore in Java (>=1.5) Die P()-Operation verringert den Wert einer Semaphore um 1, wenn ihr Wert größer null ist. Wenn nicht, wird der aufrufende Prozess suspendiert Die V()-Operation weckt, falls vorhanden, einen suspendierten Prozess auf. Ansonsten erhöht sie den Wert der Semaphore um 1 Semaphore in Java (>=1.5) public void run() { for(;;) { try { System.out.print(t.getName()); System.out.println(" trying to acquire permit"); s.acquire(); System.out.print(t.getName()); System.out.println(" acquired permit"); // kritische Arbeit for(int i=0; i<=10000000; i++); s.release(); } catch (InterruptedException e) { e.printStackTrace(); } } } import java.util.concurrent.Semaphore; public class SemaphoreThread implements Runnable { Thread t; Semaphore s; public SemaphoreThread(Semaphore s) { this.s = s; t = new Thread(this); t.start(); } public static void main(String[] args) { // 1 permit, fairness: false Semaphore s = new Semaphore(1, false); SemaphoreThread st1 = new SemaphoreThread(s); SemaphoreThread st2 = new SemaphoreThread(s); } } java.util.concurrent.Semaphore aquire() statt P() statt V() release() Konstruktor: Semaphore(int permits, boolean fair); erster Parameter gibt Anzahl erlaubter paralleler Zugriffe an zweiter Parameter gibt an, ob die Semaphore fair ist wechselseitiger Ausschluss durch binäre Semaphore möglich Quellen- und Literaturhinweise z http://www.galileocomputing.de/openbook/javainsel4 z http://java.sun.com/docs/books/tutorial/essential/threads/index.html Die Speicherhierarchie schneller kleiner Virtueller Speicher z Trennung von logischem und physikalischem Speicher – Registers – Internal Cache z Secondary Cache Ein Prozess adressiert logische (=virtuelle) Speicheradressen Diese werden vom Betriebssystem in physikalische Adressen umgesetzt Vorteile: – ein einzelner Prozess kann einen größeren Speicher adressieren als physikalisch vorhanden ist – alle Prozesse erhalten den identischen (virtuellen), flachen Speicherbereich Seitentausch Zentralspeicher Festplatte langsamer größer Backup-Medien (CD-ROM, Bandlaufwerke) Virtuelle Speicherverwaltung z z z Prozess arbeitet mit logischen Adressen, werden vom BS auf physikalische umgesetzt Virtueller Speicher wird in Seiten (pages) unterteilt Hardware-Unterstützung durch MMU – – – – z Nebeneffekte des Paging z z z Umsetzung von virtueller auf physikalische Adresse Seitenfehler (Page Fault) bei Zugriff auf ausgelagerte Seite R-Bit (Reference Bit): wird gesetzt, wenn Seite benutzt wurde M-Bit (Modified Bit): wird gesetzt, wenn Seite geändert wurde Seiten werden auf Anforderung vom BS bereitgestellt (Demand Paging) Paging kann zu Verzögerungen des Programms führen → ungeeignet für Echtzeitsysteme Paging ist transparent für den Benutzer. Aber: Nebeneffekte sind möglich! Beispiel: Seitengröße 4kByte; Programm benötigt Feld mit 1024 mal 1024 Integer-Zahlen int i, j, a[1024][1024]; // schnelle Schleife: for(i=0; i<1024; i++) for(j=0; j<1024; j++) a[i][j] = 0; z // langsame Schleife: for(i=0; i<1024; i++) for(j=0; j<1024; j++) a[j][i] = 0; Links 1024 Seitenanforderungen, rechts 1024*1024 ! Seitentauschstrategien z Problem: wenn kein freier Seitenrahmen vorhanden ist, wie kann eine neue Seite eingelagert werden? – Es muss zunächst eine andere entfernt werden! Optimale Strategie z z Welche Seite kann zuerst entfernt werden? z Ziel: möglichst wenig Seitenfehler – – Verschiedene Seitentauschstrategien Implementierung muss mit vorgegebener Hardware möglich sein Seitentauschstrategien - Benchmark z z z z First-In First-Out (FIFO) Ziel: möglichst wenig Seitenfehler Implementierung muss mit vorgegebener Hardware möglich sein Test einer Seitentauschstrategie, indem eine spezielle Seitenanforderungsfolge (reference string) durchlaufen und die Anzahl der Seitenfehler gemessen wird z Beispiel: 0, 1, 2, 4, 0, 1, 3, 0, 1, 2, 4, 3 Seitenanforderungsfolgen können zufällig mit vorgegebenen Verteilungen erzeugt werden, oder von echten Programmläufen gewonnen werden z – – Optimale Strategie: Suche die Seite, die am spätesten wieder benutzt wird Im Allgemeinen unmöglich zu implementieren, da keine Vorhersage in die Zukunft möglich ist Ist jedoch wichtig als Vergleichsmöglichkeit für andere Seitentauschstrategien z z z Die Seite wird entfernt, die als erste geladen wurde Einfach zu implementieren Nur minimale Hardware-Unterstützung nötig Anfällig für Belady‘s Anomalie: in seltenen Fällen kommt es bei mehr Seitenrahmen zu einer Erhöhung der Seitenfehler (Belady et.al., 1969) Wurde in den ersten VAX/VMS Rechnern verwendet Belady‘s Anomalie Belady‘s Anomalie Belady‘s Anomalie Least-Recently Used (LRU) z Seitenanforderungsfolge:0,1,2,3,0,1,4,0,1,2,3,4 z z 0 3 4 0 4 3 1 0 2 1 0 4 2 1 3 2 1 3 2 9 Seitenfehler (Optimal: 7) z 10 Seitenfehler (Optimal: 6) (zweiter Fall: 012401301243) z Suche die Seite, die am längsten unbenutzt war Kommt der optimalen Strategie nahe, ist jedoch sehr aufwändig zu implementieren LRU Simulationen in Software: – – z Verwendung von Zählern Stack Algorithmen Alle LRU-Algorithmen sind immun gegen Belady‘s Anomalie LRU Näherung: Zählerbasierte Tauschstrategien z LFU (Least Frequently Used): Für jede Seite wird ein Zähler mitgeführt; wurde die Seite benutzt (d.h. R-Bit ist 1), so wird der Zähler erhöht. – – z Die Seite mit dem niedrigsten Zählerstand wird entfernt Problem: LFU vergisst nicht: unbenutzte Seiten, die vor langer Zeit mal stark genutzt wurden, verbleiben im Speicher Altern (Aging): bei unbenutzten Seiten wird der Zähler vermindert: – Exponentielles Altern: z = z >> 1; if( r_bit > 0 ) z += 128; r_bit = 0; – Lineares Altern: if( r_bit > 0 ) z += 3; else if( z>0 ) z--; r_bit = 0; Quellen- und Literaturhinweise z „Modern Operating Systems“, 2nd Edition, Andrew Tanenbaum, Prentice Hall, 2001 (deutsch: „Moderne Betriebssysteme“ im Verlag Pearson Studium) z „Operating System Concepts“, 6th Edition, A. Silberschatz et.al, Wiley, 2003 z Informationen zu Linux VM unter www.linux-mm.org, insbesondere „Too little, too slow“ von Rik van Riel Zusammenfassung Seitentauschstrategien z z z z z Optimale Strategie nicht umsetzbar FIFO: einfach, nur minimale HardwareAnforderungen, aber Belady Anomalie LRU: gut, schwer zu implementieren LRU Näherungen: Second Chance, LFU, Aging, u.a. Seitentauschstrategien sind wichtige Teile der komplexen virtuellen Speicherverwaltung