Paradigmen der Programmierung Nebenläufigkeit Prof. Dr. Christian Kohls Informatik, Soziotechnische Systeme 2. Threadsicherheit • Primitive Thread-Operationen in Java • Parallelität in Berechnungen • Probleme mit Wettlaufbedingungen • Threadsicherheit: • Unveränderliche Objekte • Atomare Operationen • Möglichst wenig gemeinsame Variablen • Threadlokale Variablen • Objektsperren Nebenläufige Programmierung Ungewohnte Programmierung, aufgrund von - Nichtdeterminierten Abläufen - Wettlaufbedingungen - Möglichen Deadlocks - Unerwarteten Effekten aufgrund von Compileroptimierungen Viele Probleme, die sich aber reduzieren lassen durch – Funktionale Programmierung (=> Zustandslosigkeit) – Botschaftenaustausch (=> Weniger Wettbewerbssituationen) – Frameworks und Bibliotheken (Optimiertes Design) • • • • Aus Effizienzgründen sind oft besondere Lösungen nötig. Primitivoperationen können auch die Grundlage für Frameworks sein. Primitivoperationen sind fehleranfälliger! Man kann sie aber beherrschen, wenn man sich auskennt! Threads mit Java Threadausführung und die Elemente eines Java-Programms Die Ausführung eines Java-Programms umfasst: • Klassen zur Beschreibung von Objekten • Objekte zum Speichern von Information • Methoden, die sagen, wie etwas gemacht wird • Einen Thread der sich durch die Methoden durchschlängelt und jeweils genau eine Methode ausführt. Der Main-Thread startet mit der Funktion main() • Eventuell weitere Threads (automatisch oder programmiert) (Multithreading) • Verschiedene Speicherbereiche (Stack, Heap, Register, Cache) • Den Prozess der virtuellen Maschine in dem sich das alles abspielt Java ist so entworfen, dass man architekturunabhängig korrekte Multithreading-Programme schreiben kann – aber das ist nicht einfach!! Automatische Threads in Java Unabhängig von besonderen Anweisungen des Programms enthalten Java-Programme mehrere Threads: • main-Thread, der das eigene Programm ausführt. • Threads für die automatische Speicherbereinigung. • Event-Thread für die Behandlung der Benutzerinteraktion. Diese automatischen Threads stellen (in einfachen Programmen) keine Probleme dar. Explizites Erzeugen von Threads • • Java-Threads müssen direkt oder indirekt die Schnittstelle java.lang.Runnable implementieren und damit über eine Methode run() verfügen. Die Methode run() wird beim Start des Thread aufgerufen. Sie stellt praktisch die jeweilige main-Funktion dar. package java.lang; public interface Runnable { public void run(); } Um einen Thread zu erzeugen, benötigt man die Hilfe der Klasse Thread, die ebenfalls bereits die Schnittstelle Runnable implementiert. Es ergeben sich unterschiedliche Möglichkeiten Threads zu erzeugen: • Statische Erweiterung der Klasse Thread durch Vererbung • Dynamische Erweiterung der Klasse Thread durch Delegation (besser) • Erzeugen von Threads durch anonyme Klassen (Funktionsobjekt) Runnable interface Delegation Lebenszyklus • run() enthält Anweisungfolge für den Thread – wie main() • Thread erzeugen: Thread-Objekt existiert, aber ist noch nicht Ablaufbereit • start() aufrufen: Thread ist ausführbar • Achtung: start() und nicht run() aufrufen! • Thread endet wenn – Code in run() abgearbeitet ist – Thread als Daemon arbeitet und keine anderen Threads mehr laufen • stop() ist deprecated und nicht sicher • Andere Threads können interrupts senden, aber dies dient nur als „Hinweis“ – Thread muss sich selbst stoppen, d.h. Code beenden Die Klasse Thread class Thread { static Thread currentThread() void run() void start() static void sleep(long millis) static void yield() Thread.State getState() boolean isAlive() boolean isInterruped() join() setDaemon(boolean b) void setPriority(int new Priority) ... Vereinfachtes Zustandsdiagramm der Threadausführung lock wait() sleep() unlock notify() start() Scheduler Ablauf fertig interrupt() NEW: Neuer Thread, nicht gestartet RUNNABLE: Läuft in der JVM wenn Rechenzeit zugeteilt BLOCKED: Wartet auf das Eintreten in einen kritischen Bereich (Monitore) WAITING: wartet auf notify() TIMED_WATING: nach sleep() TERMINATE: Beendet Interrupts Interrupts und InterruptedException Fehlerhaftes aktives Warten (busy waiting – schlecht!!) Die einfachste Idee ist, in einer while-Schleife immer wieder die Bedingung abzufragen: Das vergeudet Prozessorzeit und ist falsch: while (q.isEmpty()) /* nichts */ ; Das ist falsch: while (q.isEmpty()) Thread.yield() ; Auch das ist falsch: while (q.isEmpty()) Thread.sleep(delay) ; Die meisten Anwendungen des aktiven Wartens vergeuden Prozessorzeit. Sie sind zudem falsch, wenn nicht auf die Sichtbarkeit der Bedingungen geachtet wird. Monitor-Mechanismen für passives Warten Alle nötige Funktionalität wird von der Klasse Object geerbt. Daher kann jedes Objekt für eine Wartebedingung stehen. Allerdings muss die ausführende Thread zum Zeitpunkt des Aufrufs dieser Methoden über die Sperre des Objekts verfügen (wird zur Laufzeit überprüft). obj.wait(); Warte und gib die Sperre frei. Wenn der Thread ein notify()-Signal erhält, versucht er die Sperre wieder zu bekommen (kann was dauern) und macht dann weiter. obj.wait(long msecs); Warte maximal msecs Millisekunden. wait() kann von außen unterbrochen werden: InterruptedException. obj.notify(); Sende einem auf obj wartenden Thread ein notify()-Signal. obj.notifyAll(); Sende allen auf obj wartenden Threads ein notify()-Signal. Hinweis: Später erfahren Sie mehr über Sperren und Monitore. Möglicheiten & Grenzen der Parallelverarbeitung 1 2 3 4 1 2 3 4 1 4 9 16 1 4 9 16 Unabhängige Daten!!! Summe berechnen 1 2 3 4 7 3 10 Kumulierte Werte in Array eintragen 1 2 3 4 Kumulierte Werte in Array eintragen 1 1 2 3 4 Kumulierte Werte in Array eintragen 1 2 1 3 3 4 Kumulierte Werte in Array eintragen 1 2 3 1 3 6 4 Kumulierte Werte in Array eintragen 1 2 3 4 1 3 6 10 Werte setzen 1 1 1 1 2 2 2 2 2 2 2 2 Werte setzen 1 1 1 1 1 2 1 1 1 2 2 2 Werte setzen 1 1 1 1 1 2 1 2 2 2 2 2 Alle inkrementieren 1 2 3 4 5 2 3 4 2 3 4 5 Alle inkrementieren 2 2 3 4 5 3 4 5 2 3 4 5 • Was passiert eigentlich beim gleichzeitigen Setzen? • Was ist die Ursache für das Problem? • Wie ist dies beim Actor-Ansatz gelöst? • Welche Probleme bleiben? Zeitliche Unbestimmtheit • Ausführungsreihenfolge innerhalb eines Threads ist sequentiell und deterministisch • Ausführungsreihenfolge zwischen mehreren Threasd ist nicht deterministisch -> Wettlaufbedingungen • Nicht-determiniert bedeutet: – Abläufe können unterschiedlich sein – Fehler treten nur manchmal auf! – Ausführungsreihenfolge lässt sich nicht beobachten (da jedes Mal anders) • Bestimmte Operationen müssen als atomare Einheit ausgeführt werden, um die Klasseninvarianz zu erhalten. Mehrere Threads können aber unabhängig voneinander gemeinsame Daten verändern. Mehrere Threads können auf dem gleichen Objekt ausgeführt werden Thread 1 Anweisung 1 Anweisung 2 Obj1.fktAlpha Anweisung 4 Anweisung 5 Anweisung 6 Anweisung 7 Anweisung 8 Anweisung 9 Obj2.fktBeta Anweisung 11 … Objekt 1 fktAlpha() { Anweisung x Anweisung y } Objekt 2 fktBeta() { Anweisung a Anweisung b } Thread 2 Anweisung 1 Obj1.fktAlpha Anweisung 2 Anweisung 3 … Optimierungen Der Compiler, die Java-Laufzeitumgebung und der Prozessor können innerhalb dieser Grenzen Modifikationen vornehmen um die Effizienz zu steigern. Und das tun sie auch!!! – Soweit es keinen Einfluss auf die Ergebnisse hat, kann die Reihenfolge von Befehlen verändert werden (reordering). – Daten können zeitweise in Registern und Cache-Speichern gehalten werden. In dieser Zeit ist der Inhalt des Hauptspeichers nicht korrekt. Es ist denkbar, dass ein Objekte niemals im Hauptspeicher erscheint (visibility) . Im Ergebnis kann erst durch diese Maßnahmen die Leistungsfähigkeit moderner Computer erreicht werden! Diese Punkte sind vielen Java-Entwicklern nicht bekannt! Folge: Programmfehler !!! Wettlaufbedingungen (Race Conditions) Probleme entstehen, sobald mehrerer Threads auf gemeinsame Variable zugreifen. Dies kommt daher, dass sowohl die Ausführungsumgebung (vom Compiler bis zur CPU) als auch die logische Sicht der Objektorientierung zunächst nur für den sequentiellen Ablauf in einem Thread ausgelegt sind. • Problem des gleichzeitigen Schreibens • Unbestimmte Reihenfolge gesetzter oder verarbeiteter Werte (Nichtdeterminiertes Timing) • Read-Modify-Write • Check-then-act Verletzung der Invarianz Wettlaufbedingungen und Invarianten In Bezug auf Korrektheit wurden im 2. Semester wichtige Grundregeln formuliert: 1. Man kann die Korrektheit einer Methode überprüfen, indem man nachvollzieht, welche Anweisungen in der Methode ausgeführt werden und welche Auswirkungen die aufgerufenen Methoden haben. 2. Zu einer Klasse gehören Invarianten, d.h. Aussagen über die erlaubten Werte der Instanzvariablen, die beim Eintritt und beim Verlassen einer Methode erfüllt sein müssen. • • Die Verwendung dieser Regeln ermöglicht es, Methoden unabhängig voneinander zu entwickeln. Wenn es möglich ist, dass während der Ausführung einer Methode (innerhalb eines Thread) ein anderer Thread eine Methode desselben Objekts aufruft, ist die Korrektheit nicht mehr garantiert. ZIEL: Daten vor unkontrolliertem gleichzeitigen Zugriff schützen, Invarianz erhalten Thread-Sicherheit (thread safety) Ein Objekt ist threadsicher wenn es korrekt funktioniert und von anderen Objekten immer in einem gültigen Zustand gesehen wird auch wenn mehrere Threads darauf zugreifen. Dies muss unabhängig vom Scheduling und der nicht-determinierten Ablaufreihenfolge gelten. Aufrufender Client-Code muss keine zusätzlichen Synchronisationsmaßnahmen treffen threadsicherheit zu wahren. Thread-Sicherheit (thread safety) • Der Zustand eines Objekts ist gegeben durch die Werte seiner Instanzvariablen! • Klassenvariable gehören zum Klassen“objekt“. Lokale Variable, gehören zu einem Thread (und sind damit unproblematisch). • Jedes Objekt sollte von außen stets in einem gültigen Zustand angetroffen werden. • Aber nicht jedes Objekt sollte threadsicher programmiert sein, da damit beträchtlicher Laufzeitoverhead verbunden ist. • Für jedes Objekt sollte bekannt sein, ob es threadsicher ist oder nicht. • Unveränderliche Objekte (z.B. Strings) oder zustandslose Objekte sind automatisch threadsicher! • Es lässt sich nicht vermeiden, dass während der Ausführungszeit einer öffentlichen Methode ein Objekt zeitweise den gültigen Zustand verlässt. Threadsicherheit bedeutet, dass während dieser „kritischen“ Zeit der Zugriff durch andere Threads verhindert wird. Maßnahmen zum Gewährleisten der Threadsicherheit • Möglichst wenig gemeinsame Daten – Objekte nur in einem Thread benutzen (confinement/Kapselung) – Threadlokale Variable nutzen • Unveränderliche Objekte – Objekte dürfen erst nach der Konstruktion bekannt werden – Konstante statt variable Werte • • • • • • Atomare Operationen nutzen (Bibliothek) Gemeinsame Variable als volatile deklarieren Kommunikation über sichere Bibliotheksklassen Minimieren der Interaktion von Threads durch einfache Architektur! Dokumentation von Strategien und Policies Zugriff zu unsicheren Objekten durch Objektsperren sichern Merke: Immer wenn mehr als ein Thread auf eine veränderbare Variable zugreifen, und mind. ein Thread diese verändert, dann müssen alle ihren Zugriff durch Synchronisation koordinieren. Maßnahme 1: Kapselung Welche Variablen sind lokal zu einem Thread, welche gemeinsam? In Java haben wir: • Lokale Variable und Funktionsparameter: Diese Variablen liegen auf dem Stack. Sie sind nur innerhalb der laufenden Methode und (damit) auch immer nur in einem Thread sichtbar. • Threadlokale Variable (Klasse java.lang.ThreadLocal) sind nur in einem Thread sichtbar. • Objekte und ihre Inhalte sind im Heap gespeichert. Sie können von allen Threads angesprochen werden. • Klassenvariable verhalten sich wie die Inhalte von Objekten. • Die Probleme der Nebenläufigkeit haben nur mit dem Zugriff auf gemeinsame Variable zu tun! • Fehler sind durch Testen kaum zu erkennen. Daher kommt es darauf an, einfach und sicher zu programmieren! • Just as encapsulation can make it easier to preserve invariants, local variables can makte it easier to confine objects to thread. Maßnahme 2: Unveränderliche Daten • • • Unveränderliche Objekte sind absolut unkritische Behälter für Werte. Wann immer möglich Variablen als final deklarieren Ggf. deep copy von Containern • • Ein Objekt ist unveränderlich, wenn alle seine Komponenten unveränderlich sind und wenn alle Variablen final sind. Unveränderliche Objekte sind threadsicher. @Immutable public class Point{ public final double x; public final double y; public Point(double x, double y) { this.x = x; this.y = y; } } Es ist bei einfachen Daten-Objekten auch nicht nötig, dass die Variablen gekapselt sind (solange keine Implementierungsgeheimnisse im Spiel sind). Unsichere Objektkonstruktion • register() macht das Objekt vor der Fertigstellung bekannt. Damit kann z.B. ein falscher Wert für die Konstante gefunden werden. • Ebenso wird durch das Starten des Threads bewirkt, dass mit einem unvollständig initialisierten Objekt gearbeitet werden kann. • Fazit: Niemals einen Thread im Konstruktor starten. Die this-Referenz nicht anderen Objekten vom Konstruktor aus bekannt machen. Warnung: Die Reihenfolge ist nicht allein schuld! Konstanten gelten erst nach Ablauf des Konstruktors als „eingefroren“. • Problematisches „lazy“ Singleton-Muster Maßnahme 3: Volatile • • • • • Allgemein gibt es keine Garantie, dass die Änderungen durch einen Thread für andere Threads sichtbar werden (Caching!) Synchroinsation für sichtbarkeit zwischen Threads erforderlich Änderungen an volatile-Variablen sind immer in der richtigen Reihenfolge sichtbar und Variablen werden nicht gecached. Zusätzlich wird ab Java 5 auch garantiert, nach dem Zugriff auf eine volatile-Variable auch davor stattgefunden Veränderungen durch den ändernden Thread sichtbar sind. Volatile garantiert auch, dass 64 bit Werte (double und long) atomar verändert werden. Maßnahme 4: Atomare Operationen • Atomares Test & Set: Atomare Test & Set – Operationen bilden den Kern für die Implementierung aller effizienten Mechanismen der Nebenläufigkeit. Auch der Zähler lässt sich so realisieren. Es bietet aber auch die Möglichkeit für die Implementierung anderer Operationen compareAndSet prüft, ob der aktuelle Wert gleich oldCount ist und setzt ihn genau dann auf den neuen Wert. Die Rückgabe informiert über den Erfolg der Aktion, die atomar abläuft. Verwendung von atomaren Referenzen für veränderliche Objekte Maßnahme 5: Objektsperren • Sperren stellen sicher, dass auf einer Ressource höchstens eine Aktivität stattfindet. • In Java werden Sperren meist über Monitore abgebildet • Es gibt einen wechselseitigen Ausschluss • Wenn ein Thread sich den Zugriff auf die Ressource gesichert hat, können keine anderen Objekte daraufzugreifen – sie müssen warten. • Derart geschützte Aktivitäten werden als kritischer Abschnitt bezeichnet. • • • Objektsperren sind manchmal erforderlich, um Invarianz zu garantierten. Aber: Verlust der Nebenläufigkeit (Blockieren, Warten) Aber: Gefahr des Deadlocks (Programm läuft nicht weiter) Implementierung von Sperren • Kritische Abschnitte können über Bibliotheksmechanismen geschützt werden. Dabei wird zu Beginn des Bereichs die Methode lock() und am Ende unlock() aufgerufen. • Alternativ kann ein Block (oder eine Methode) gemäß dem Monitorkonzept von Java durch synchronized { ... } gesichert werden. • In beiden Fällen verfügt ein Objekt über eine Sperre (lock) (beim Monitorkonzept kann das jedes beliebige Objekt sein), die den Zugang zu dem kritschen Bereich steuert. • Ein Thread, der einen ungesperrten Bereich betritt, erhält die Sperre für das zugehörige Objekt. Er darf damit alle gesamten geschützten Bereiche des Objekts betreten (auch wenn diese über mehrere Methoden verteilt sind). • Wenn er alle geschützten Bereiche verlassen hat, gibt er die Sperre wieder frei. • Diese Eigenschaft heißt auch Wiedereintrittsfähigkeit (reentrancy). • Ein Thread, der versucht, einen gesperrten Bereich zu betreten, wartet bis der Bereich frei wird und er die Sperre erhält. • Die Sperre verwaltet eine Liste von wartenden Threads (entry-set). Beim Freiwerden der Sperre wird einer der wartenden Threads ausführungsbereit. Schutz des kritischen Bereichs (Zähler-Beispiel) Besonderheiten der Objektsperre • Eine Sperre schützt kritische Bereiche und dazugehörende Variable (Invariante!). • Zusammengehörende Bereiche und Variablen müssen mit der gleichen Sperre geschützt werden. • Fremde Bereiche/Variable werden mit einer anderen Sperre geschützt. • lock() und unlock() bewirken die Sichtbarkeit der geschützten Variablen. • lock() und unlock() müssen paarweise zusammen aufgerufen werden (try…finally). • Lock schützt kritische Bereiche dynamisch. • synchronized schützt kritische Bereiche statisch. Monitore • Von Brinch-Hansen und Hoare entwickelter Mechanismus als sicherer Mechanismus für nebenläufige Programme entwickelt • Ein Monitor ist ein Zusammenschluß von Attributen und Methoden (Zugriffsoperationen). Methoden werden stets wechselseitig ausgeschlossen ausgeführt. • In Java ist das Monitorkonzept einfach aber auch unsicher implementiert. • Schützt kritische Abschnitte • Ermöglicht das Warten bis eine Bedingung eintritt • Schlüsselwort synchronized – – • • • Bei Methoden wird this als Sperre verwendet Bei synchronized Blöcken können andere Sperren verwendet werden – jedes Objekte kann als Sperre funktionieren Voneinander abhängige Variablen identifzieren und Zugriffe auf diese in kritische Abschnitte legen Gemeinsame, veränderliche Variablen sollten immer von genau EINER Sperre geschützt werden! Sperren blocken NICHT den Zugriff auf ein Objekt sondern blockieren solange bis eine Sperre freigegeben ist! Das this-Objekt verwaltet die Objektsperre. Synchronized erklärt den Körper einer Methode zum kritischen Bereich. Synchronized funktioniert reentrant und garantiert aktuelle Werte der Variablen. Synchronized sorgt automatisch für die Freigabe der Sperre. Synchronized ist nicht Teil der Signatur! Das angegebene Objekt verwaltet die Objektsperre. Es ist ein beliebiges Objekt. Synchronized erklärt den Körper einer Blocks zum kritischen Bereich. Die beiden Varianten können kombiniert werden, sofern sie sich auf dasselbe Objekt beziehen. Diese Variante bietet die Möglichkeit, nur Teile einer Methode zu sichern. Beispiel für Ablauf mit synchronized Probleme der Objektsperre Idee des Sicherheitsfanatikers: „grundsätzlich sollten alle Methoden gesichert sein, damit keine Interferenz auftritt“!? Diese Idee ist schlecht: • Sperren erfordert Aufwand und senkt dadurch die Effizienz. • Sperren erhöht bei paralleler Hardware die Zeiten bloßen Wartens, also senkt es nochmals die Effizienz. • Sperren bedroht die Lebendigkeit eines Programms. – Lebendigkeit: „ein Programm ist lebendig, wenn alle geplanten Aktivitäten irgendwann ausgeführtwerden.“ – Deadlock: Mehrere Threads warten auf die Freigabe von Ressourcen und blockieren sich dabei gegenseitig. Hier geht es um die Freigabe von Sperren. – Bei einem Deadlock sind mindestens zwei Threads und mindestens zwei Objektsperren beteiligt. Deadlock-Beispiel Möglicher Ablauf: 1. a startet run() und erhält die Sperre von a. 2. b startet run() und erhält die Sperre von b. 3. a versucht tueWas() zu rufen und wartet auf die Sperre von b. 4. b versucht tueWas() zu rufen und wartet auf die Sperre von a. 5. Die beiden Threads warten gegenseitig aufeinander (deadlock) Deadlock vermeiden (Beispiel) Hier kann kein Deadlock entstehen, da das Objekt seine Sperre freigibt, ehe es ein anderes Objekt aufruft. Also kann a auf b warten. Aber nicht gleichzeitig b auf a. Nach Block1 muss das Objekt in einem erlaubten Zustand sein (Klasseninvariante). Einfache Faustregeln zum Vermeiden von Deadlocks: Die folgenden Regeln gelten für Klassen, deren Objekte gleichzeitig von mehreren Threads angesprochen werden können: • Sperre immer fremden Zugriff, wenn Instanzvariablen eines Objekts verändert werden. Andernfalls können unstimmige Veränderungen vorgenommen werden. • Sperre immer fremden Zugriff beim Lesen von Instanzvariablen, die von anderen Threads verändert werden könnten. Anderfalls können unstimmige Werte zurückgegeben werden (Alternative: volatile). • Sperre fremden Zugriff nach Möglichkeit nicht, wenn Methoden auf anderen Objekten aufgerufen werden. Andernfalls können Verklemmungen (deadlock) entstehen. Die Objektsperre ist ein mächtiger und gefährlicher Mechanismus. Gemeinsame Objekte sollten sich möglichst einfach verhalten! Maßnahme 6: Dokumentation • Kennzeichnen, welche Klassen threadsicerh sind! • Policies dokumentieren • Worüber wird geblockt? • Annotationen verwenden: @NotThreadSafe @ThreadSafe @Immutable (unveränderlich = automatisch threadsicher) @GuardedBy("…") (wird durch ein angegebenes Monitor-Objekt geschützt) NotThreadSafe ThreadSafe ThreadSafe NotThreadSafe Merke: Die Verwendung mehrerer threadsicherer Komponenten macht eine Klasse nicht automatisch threadsicher: – Es können Abhängigkeiten zwischen den threadsicheren Variablen bestehen. – Wenn eine Methode mehrere threadsichere Methoden aufruft, dann müssen diese unter Umständen synchronisiert werden (z.B. Atomizität) Nicht threadsichere Komponenten können oft leicht threadsicher gemacht werden: - Unveränderbarkeit des Objekts - Sicherstellen, dass nur eine Thread auf das Objekt zugreift - Wrapping in threadsichere Objekte Ausblick • • • • Synchronisation von Thread-Abläufen Einsatz von Klassenbibliotheken Thread-Pools und Performance Frameworks