Vorlesungsfolien 7. November

Download Report

Transcript Vorlesungsfolien 7. November

Paradigmen der Programmierung Teil 2: Nebenläufigkeit

Prof. Dr. Christian Kohls Informatik, Soziotechnische Systeme

1. Das Actor-Konzept in Scala

• Das Actor Modell • Nebenläufigkeit in Scala • Erzeugen und Starten eines Actors • Datenaustausch • Asynchrone Kommunikation • Synchrone Kommunikation • Future Objekte

2. Einführung in die Nebenläufigkeit

• Grundbegriffe •

Multitasking, Prozesse und Threads

• Parallele Rechnerarchitekturen • •

Threads in Java Threads erzeugen verwalten und beenden

• Threadzustände • Threadsicherheit • Sichere Konstruktoren • Unveränderliche Objekte • Atomare Operationen • Sichere Verwendung von einfachen Variablen • Threadlokale Variablen

Motivation für Nebenläufigkeit

1 2 3 4 1 2 3 4 1 4 9 16 1 4 9 16 • Parallele Verarbeitung von Daten (z.B. Cloud Computing, Big Data Analyse) • Effiziente und parallel arbeitende Webserver • Keine Blockierung des Systems durch langsame Operationen (I/O, Serveranfrage) • Ausnutzung von Multicore-Architekturen und verteilten Systemen • Simulation realer Objekte oder Ereignisse • Unabhängiger Kontrollfluss z.B. für graphische Oberflächen oder Garbage Collection …

Organisation von Prozessen und Threads durch das Betriebssystem

Prozess 1 Prozess 2 Prozess n Thread 1 Thread 2 Thread n Thread 1 Thread 2 Thread n Thread 1 Thread 2 Thread n Betriebbsystem CPU 1 Core Core CPU 2 Core Core CPU n Core Core

Ein Prozess ist ein in der Ausführung begriffenes Programm

• Ein Programm beschreibt

statisch

Struktur und Ablauf.

• Ein Prozess ist die

dynamische

Ausführung des Programm.

Statischer Code:

int i = 10 int y = i *3

Dynamischer Ablauf:

new new 1. Thread 2. Thread 3. Thread

Was sind Threads?

Ein

Thread

ist eine Ausführungsreihenfolge innerhalb eines Prozesses. Ein Prozess enthält mindestens 1 Thread (sequentieller Prozess). Ein Prozess kann mehrere Threads enthalten.

Jeder Thread hat einen separaten Stack (Rücksprung, lokale Variablen) und eine eigene unabhängige Ablaufkontrolle (Programmzähler) Die Threads eines Prozesses können gemeinsamen Speicher des Prozesses nutzen.

> Möglichkeit des Datenaustausches -> Effizientes Umschalten zwischen Threads (durch OS) -> Aber: Synchronisationsprobleme!

Ein Thread ist ein Ablauf.

In Java können Methoden verschiedener Objekte in einem einzigen Thread ausgeführt werden. Umgekehrt kann eine Methode in verschiedenen Threads aufgerufen und 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 }

Ein Thread ist ein Ablauf.

In Java können Methoden verschiedener Objekte in einem einzigen Thread ausgeführt werden. Umgekehrt kann eine Methode in verschiedenen Threads aufgerufen und 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 …

Threads in Java

• •

Objekte gehören vom Grundsatz her nicht zu einem bestimmten Thread!!!

Ein Thread befindet sich zu einem bestimmten Zeitpunkt in der Methode eines Objekts (

aktuelles this-Objekt

).

• Jeder Thread verfügt über ein Objekt, dass wichtige Informationen über den Thread speichert und wichtige Aktionen verfügtbar macht. Dieses Objekt ist eine

Instanz der Klasse Thread

(oder einer davon abgeleiteten Klasse) • Es gibt

Klassenmethoden der Klasse Thread

(z.B. Thread.currentThread() ).

• Der Threadablauf startet immer in der

Methode run()

eines Objekts, das die

Schnittstelle Runnable

implementiert. Der Thread wird spätestens am Ende von run() beendet.

• Thread implementiert das Interface Runnable – Threads können den Ablauf an Runnables delegieren

oder

– Unterklassen von Threads können direkt in run() einen Ablauf festlegen

Runnable interface

Vereinfachtes Zustandsdiagramm der Threadausführung

unlock notify() lock wait() sleep() start()

Scheduler Ablauf fertig

interrupt()

Threads in Java (fortg.)

Threads stellen einen

Ablauf

dar, keinen besonderen Sichtbarkeits- oder Speicherbereich.

Ein Methode wird durch einen Thread ausgeführt.

Die lokalen Variablen dieser Methode sind nur lokal in der Methode und damit automatisch nur während ihrer Ausführung in

einem

Thread bekannt.

Objekte

(auf dem Heap) können grundstätzlich von allen Threads angesprochen werden. Statische und nichtstatische Variable von

Klassen

können von unterschiedlichen Threads gelesen oder geschrieben werden.

Der Zugriff auf

gemeinsame Variable

bedarf besonderer Vorkehrungen!

Manchmal muss ein Thread

warten

bis ein bestimmter Zustand eingetreten ist.

Die Thread Ausführung wird durch das

Scheduling

gesteuert. Java legt nicht fest, wie dies geschieht.

Threads in Java

Genau wie bei main() werden die Anweisungen eines Threads nacheinander abgearbeitet Mehrere Threads können nun parallel (nebenläufig) arbeiten Die Ablauffolge der Anweisungen innerhalb eines Threads ist klar Die Ablauffolge von Anweisungen verschiedener Treads ist nicht determiniert

Keine determinierten Wechsel zwischen Treads

Klare Folge innerhalb eines Threads Nichtdeterminierte Folge

zwischen

zwei Threads Achtung: Internes Umordnung und Zwischenspeichern im Cache (Visibilty) durch Compiler möglich!

Beispielprogramme in Java

Echos Buchstabenausgabe Drei gleiche Werte

Achtung: Deadlocks!

Ich bin jetzt oben!

Ich möchte runter.

Ich bin jetzt unten!

Ich möchte hoch.

person1.blockiereOben() person1.blockereUnten() person2.blockiereUnten() person 2.blockereOben()

Problem bei „Drei gleiche Werte“ Problem: Lösung:

Wettlaufbedingungen / Race Conditions Synchronisation

Konsequenz:

Gefahr eines Deadlocks Overhead Eingeschränkte Nebenläufigkeit Schwer wartbarer Code   Synchronisation so viel wie nötig, so wenig wie möglich Wir werden noch verschiedene Lösungsstrategien kennenlernen

Problem der gleichzeitigen Nutzung von Daten

5 Äpfel im Korb

Actors – selbständige Akteure

Actor „Person 1“ Actor „Person 2“ Actor „Apfelkorb“ Actor „Verbraucher“

Actors – selbständige Akteure

Actor „Person 1“ Actor „Person 2“ Actor „Apfelkorb“ Actor „Verbraucher“

Das Actor-Modell von Scala

• • • • • • • • • • • • Beim Actor-Modell werden Botschaften zwischen Objekten ausgetauscht Statt Threads werden Akteure (Actors) erzeugt Akteure agieren nebenläufig Akteure werden von außen nur über Nachrichten angesprochen.

Aktoren verfügen nicht über gemeinsame veränderliche Daten

(in der funktionalen Variante haben sie keine veränderlichen Daten) Die Kommunikation zwischen Aktoren findet über (unveränderliche)

Nachrichtenobjekte

statt.

Aktoren verfügen über eine

Message Queue („Mailbox)

aus der sie die erwarteten Nachrichten auswählen.

Liegt keine passende Nachricht vor, dann blockiert der Aktor.

Das Versenden einer Nachricht ist nicht blockierend (

asynchron

). Der Empfänger kann später eine Antwort an den Absender zurücksenden.

Es gibt synchrone

Nachrichten

, bei denen der Sender unmittelbar auf die Antwort wartet.

Als Variante kann die synchrone Antwort in der Referenz auf ein

Future-Objekt

bestehen.

Es gilt als guter Stil, wenn das Versenden und Empfangen von Nachrichten in Methoden gekapselt ist (

Muster des aktiven Objekts

).

Einen einfachen Actor erzeugen und starten

Akteure können Nachrichten empfangen

Fallunterscheidungen (Art der Nachricht) über match

Botschaftenkanäle entkoppeln Objekte

• 1. Kein gemeinsamer Speicher, also keine Race Conditions • 2. Bewirkt gleichzeitig passives Warten auf Daten • 3. Asynchrones Senden vermeidet Deadlocks • 4. Future-Objekte ermöglichen auch asynchrones Empfangen • 5. Das Konzept lässt sich auf verteilte Systeme übertragen

Code-Beispiel

Apfelernte mit Akteuren

Threads und Objektorientierung

• In

sequentiellen OO-Programmiersprachen

ist zu jedem Zeitpunkt

genau eine Methode aktiv

.

• In dem entgegengesetzten

Actor-Konzept

sind

alle Objekte gleichzeitig aktiv

.

• Kommunikation erfolgt durch den

Austausch von Nachrichten

.

• Thread-Modelle der

prozeduralen Welt

verlegen die

Nebenläufigkeit auf die Ebene von Prozeduren:

In dem

Java

Modell können Methoden

run()

, (Schnittstelle

Runnable

), als nebenläufiger Thread gestartet werden.

Aktive Objekte

• Haben eine Methodenschnittstelle wie andere Objekte • Methoden werden in einem eigenen Thread ausgeführt • Methoden ohne Rückgabe kehren sofort zurück • Methoden mit Rückgabe kehren

in der Regel

sofort zurück • Evtl. wird ein Future-Objekt zurückgegeben

Futures

• Unter Futures versteht man Objekte, die die Ergebnisse einer erst in der Zukunft abgeschlossenen Berechnung transportieren.

• • • Futures ermöglichen es, dass eine in einem anderen Thread ausgeführte Methode unmittelbar ein Ergebnis zurückgibt.

Erst beim Zugriff auf den Ergebniswert muss (evtl.) gewartet werden.

Mittels Futures lassen sich Blockierungen und Deadlocks vermeiden.

• Futures bilden eine Grundlage für das Muster „aktiver Objekte“ • Scala: !! (anstelle von !?) liefert ein Future-Objekt

Parallele Berechnung mit Akteuren

Liste empfangen Organizer Quadrierer Durchschnittsmensch 1 2 3 4 Quadrieren 1 4 9 16 S = 30 D = 7,5 Summe und Durchschnitt berechnen Asynchrone und synchrone Kommunikation, Futures

Asynchrone Kommunikation

Listen Organizer Arbeitet Schaut ins Postfach Findet Liste im Postfach Sendet Liste an Quadrierer

Arbeitet weiter

Findet Quadrierte Liste im Postfach, sendet diese an Durchschnittsmensch,

Arbeitet weiter

Arbeitet Listen Quadrierte Liste Quadrierte Liste Quadrierer Arbeitet Schaut ins Postfach Findet Liste im Postfach Quadriert die Liste Schickt die Liste an den Organizer,

Arbeitet weiter

Arbeitet Durchschnittsmensch

Synchrone Kommunikation

Listen Organizer Arbeitet Schaut ins Postfach Findet Liste im Postfach Sendet Liste an Quadrierer

Arbeitet nicht weiter Wartet auf das Ergebnis Schaut sich Ergebnis an und entscheidet.

z.B. Quadriete Liste an Durchschnittsmenschen senden:

Arbeitet nicht weiter Wartet auf Ergebnis

Arbeitet Listen Quadrierte Liste Quadrierer Arbeitet Schaut ins Postfach Findet Liste im Postfach Quadriert die Liste Schickt die Liste an den Organizer,

Arbeitet weiter

Arbeitet Durchschnittsmensch

Futures

Listen Organizer Arbeitet Schaut ins Postfach Findet Liste im Postfach Sendet Liste an Quadrierer

Erhält Future Objekt Arbeitet weiter Zugriff auf Future Objekt Weiterarbeiten sobald Ergebnis vorliegt

Arbeitet Listen Quadrierte Liste Quadrierer Arbeitet Schaut ins Postfach Findet Liste im Postfach Quadriert die Liste Schickt die Liste an den Organizer,

Arbeitet weiter

Arbeitet Durchschnittsmensch

Zusammenfassung des Nachrichtenmodels

• • • Keine gemeinsamen Variablen Jeder Actor ist selbst dafür verantwortlich, seine Daten zu verwalten Nachrichten sind in Objekte gekapselt: es können beliebige unveränderliche Objekte übertragen werden.

• • • • • Nachrichten werden in „Mailbox“ gespeichert und nacheinander abgearbeitet Empfangen sucht eine passende Nachricht aus (partiell definierte Funktion) Wenn kein case-Muster passt, wird gewartet.

Es wird immer nur eine Nachricht bearbeitet.

Mittels reply(Ausdruck) kann eine Antwort gesendet werden • • •

!

bedeutet Senden

!?

bedeutet Senden und Warten auf reply-Antwort

!!

bedeutet Senden und Rückgabe eines Future-Objekts, mit dem man später die Antwort erhält • • Falsche Nachrichten werden nie bearbeitet – keine Prüfung durch Compiler !!

Daher Muster „Aktives Objekt“ (Nachrichtenaustausch gekapselt)

Actors - Vorteile

• Bildet paralalleles Arbeiten in der Welt ab • Vermeidet Wettlaufbedingungen, da keine gemeinsamen Variablen • Kapselung wie bei Objektorientierung • Reduziert Deadlock-Probleme

Actors – Dont‘s

• Actor sollten nicht blockieren • Kommunikation nur über Nachrichten • Nachrichten sollten unveränderliche Werte sein • Nachrichten sollten alle erforderlichen Daten enthalten – ggf. redundant

Leicht- oder schwergewichtige Akteure

Normale Akteure sind schwergewicht: – Jeder Actor erhält seinen eigenen Thread und damit Call Stack – Erzeugen und Wechseln zwischen Threads sind „teure“ Operationen • • • Für „leichtgewichtige Akteure“ react statt receive verwenden Ermöglicht es, mehrere Actors auf einem Thread auszuführen  Performanz und weniger Speicherbedarf, da kein Call Stack für jeden Actor benötigt wird Actors werden auf Thread-Pool verteilt • • • react ist wie receive eine partielle Funktion Unterschied: Kein Rücksprung nach dem Abarbeiten!!!

Konsequenz: – Alle Aufräumarbeiten müssen von der Nachrichtenbearbeitung selbst durchgeführt werden – Oft Delegation an Hilfsfunktionen oder an act selbst – Unterstützung durch Bibliotheksfunktionen loop, andThen möglich

Historie des Botschaftenaustauschs

• Immense Komplexität von Nebenläufigkeit mit gemeinsamen Speicher • Actor Modell (Hewitt 1973) • CSP = communicating sequential processes (Hoare 1978) • Botschaftenaustausch bei Parallelrechnern • Botschaftenaustausch oft in visuellen Systemen (GUI, Multimedia) • Renaissance von gemeinsamen Speicher mit Java • Actor-Modell wird mit Erlang und Scala populär (ab 2000) • Akka ist die aktuellere Bibliothek • Frameworks wie Play! setzen auf Aktoren

Zusammenfassung Nebenläufigkeit

• • • • • • Unter Nebenläufigkeit versteht man die gleichzeitige oder

quasigleichzeitige Ausführung

von Programmen oder Programmteilen.

In einem nebenläufigen Programm ist die Reihenfolge der Befehlsausführung nicht vollständig festgelegt. Das Programm kann durch einen oder mehrere Prozessoren ausgeführt werden.

Nebenläufige Abläufe

können

über

gemeinsamen Speicher

verfügen.

Gemeinsamer Speicher ist effizient, stellt aber ein großes Problem dar. Gründe sind die

sequentielle Optimierung

der Ausführung (s.o.) und die nötige Koordination in der

Änderung von Variablen

, die durch die Programmlogik bestimmt ist (Auschluss von Wettlaufbedingungen –

race conditions

).

Die Regelung der Veränderung und der Sichtbarkeit der Werte von gemeinsamen Variablen wird durch

Synchronisation

erreicht.

Programme die unabhängig von den Ausführungsmöglichkeiten eine nebenläufigen Programms immer das gleiche Verhalten zeigen, sind

threadsicher

.

Extra

• • • • • • • • Zunächst werden nur relativ einfache Konstrukte besprochen!

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.

Es entstehen eine Reihe von Problemen im Zusammenhang von: –

Reordering

von Operationen –

Sichtbarkeit

der Änderung von Variablen –

Atomizität

von Operationen Beibehaltung von

Klasseninvarianten

Notation: Threadsichere Klassen kennzeichne ich durch die selbstdefinierten Annotationen

@ThreadSafe

(

threadsicher

) und

@Immutable

(

unveränderlich

).

Gelegentlich hebe ich die Unsicherheit einer Klasse durch die Annotation

@NotThreadSafe

hervor. Später kommt noch

@GuardedBy

hinzu.

• • • • • Fast alle Probleme der Nebenläufigkeit verschwinden bei – Funktionaler Programmierung – Botschaftenaustausch – Frameworks (Fork-/Join etc.)

Aus Effizienzgründen sind oft besondere Lösungen nötig.

Primitivoperationen können auch die Grundlage für Frameworks sein.

Diese sind fehleranfälliger!

Man kann sie aber beherrschen, wenn man sich auskennt!

• • • • • • • • • • • • • •

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 Objektorientierung 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!

Thread-Sicherheit (thread safety)

• • • • • • • • • • • • • • Definition: Ein Objekt ist threadsicher, wenn es von anderen Objekten, selbst in

einer multithreading Umgebung, immer in einem gültigen Zustand gesehen wird.

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) 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.

Es ist nicht schlimm, wenn das Objekt komisch aussieht, solange das keiner sieht!

Maßnahmen zum Gewährleisten der Threadsicherheit

• • •

Objekte nur in einem Thread benutzen (confinement)

• Threadlokale Variable nutzen

Objekte dürfen erst nach der Konstruktion bekannt werden Unveränderliche Objekte verwenden.

Atomare Operationen

nutzen (Bibliothek) • Gemeinsame Variable als

volatile

deklarieren • Zugriff zu unsicheren Objekten durch

Objektsperren

sichern •

Kommunikation

über sichere Bibliotheksklassen • Minimieren der Interaktion von Threads durch

einfache Architektur

!

• Zunächst bespreche ich die einfacheren Mechanismen. Die Objektsperre ist ineffizient und gefährdet die Lebendigkeit eines Programms.

Kennzeichnung von Objektsicherheit

• In den Beispielen kennzeichne ich die Threadsicherheit durch Annotationen • •

@NotThreadSafe

• •

@ThreadSafe

• •

@Immutable

(unveränderlich = automatisch threadsicher) • •

@GuardedBy("…")

(wird durch ein angegebenes Monitor Objekt geschützt)