Die Präsentation wird geladen. Bitte warten

Die Präsentation wird geladen. Bitte warten

Paradigmen der Programmierung Nebenläufigkeit

Ähnliche Präsentationen


Präsentation zum Thema: "Paradigmen der Programmierung Nebenläufigkeit"—  Präsentation transkript:

1 Paradigmen der Programmierung Nebenläufigkeit
Prof. Dr. Christian Kohls Informatik, Soziotechnische Systeme 3. Synchronisation sequentieller und paralleler Aufgaben Erzeuger – Verbraucher - Muster Ringpuffer mit Semaphoren Phasenbezogene Mechanismen / Synchronisationspunkte Programme in Aufgaben organisieren Futures, Callables und Executors Thread Pools Performance und Skalierbarkeit

2 Erzeuger-Verbraucher-Muster
Teller abtrocknen Geschirr sortieren Teller waschen Statistik führen Logfile schreiben Dokument analysieren Antwort berechnen Dateien einlesen Request empfangen

3 Erzeuger-Verbraucher-Muster
Teller abtrocknen Geschirr sortieren Teller waschen Statistik führen Logfile schreiben Dokument analysieren Antwort berechnen Dateien einlesen Request empfangen

4 Erzeuger-Verbraucher-Muster
Teller abtrocknen Geschirr sortieren Teller waschen Statistik führen Logfile schreiben Dokument analysieren Antwort berechnen Dateien einlesen Request empfangen

5 Erzeuger-Verbraucher-Muster
Teller abtrocknen Geschirr sortieren Teller waschen Statistik führen Logfile schreiben Dokument analysieren Antwort berechnen Dateien einlesen Request empfangen

6 Erzeuger-Verbraucher-Muster
Teller abtrocknen Geschirr sortieren Teller waschen

7 Erzeuger-Verbraucher-Muster
Teller waschen Teller abtrocknen Geschirr sortieren Teller waschen

8 Erzeuger-Verbraucher-Muster
Wenn die Produzenten zu wenig Arbeit generieren muss ggf. das Verhältnis geändert werden Achtung: Wenn Produzenten zuviel Arbeit generieren, die von den Konsumenten nicht bewältigt wird, dann muss die Produktion gedrosselt werden. Lösung: Bounded Queues – Begrenzte Warteschlangen (z.B. Ringspeicher)

9 Erzeuger-Verbraucher-Muster
Ziele: Trennung von Aufgabenerstellung (Erzeugen) und Aufgabenausführung (Verbrauchen) Trennung zwischen unabhängigen Arbeitsschritten Platzieren von Arbeitsaufträgen in einer Todo-Liste für späteres Bearbeiten Vereinfacht die Entwicklung: Codeabhängigkeiten zwischen Produzent und Verbraucher werden reduziert Vereinfacht das Workload Management: Daten können mit unterschiedlicher Geschwindigkeit erzeugt/verbraucht werden können

10 Erzeuger-Verbraucher-Muster
Varianten: Ein Produzent – Mehrere Verbraucher (z.B. Aufteilen der Liste in Einzelaufgaben, Dateien einlesen, Analyse verteilen) Mehrere Produzenten – Ein Verbraucher (z.B. Apfelkorb, Logfile, Zusammenführen von Teilergebnissen) Mehrere Produzenten – Mehrere Verbraucher (z.B. Web-Crawler: mehrere Seiten parallel Laden, mehrere Seiten parallel analyisieren) Beziehungen sind relativ: Eine Aktivität kann sowohl Verbraucher wie auch Erzeuger sein

11 Bounded Queues Mächtiges Resourcemanagementwerkzeug für zuverlässige Anwendungen Programme werden robuster Drosseln von Aktivitäten, die mehr Arbeit produzieren als bewältigt werden können Am Besten gleich zu Anfang einplanen und nicht erst wenn das System an die Grenzen stösst! Serielle Threadsicherheit durch Weitergabe des Objektbesitzes: Produzent gibt Objekt in Queue und sollte nicht länger darauf verweisen, Verbraucher nimmt Objekt aus Queue Nur Produzent ODER Verbrauchern besitzen eine Referenz auf das Objekt, so dass immer nur ein Thread darauf zugreift! Einsatzbereite Bibliotheksklassen: BlockingQueue als neuer Collectiontyp implementiert u.a. als ConcurrentLinkedQueue

12 Live Codebeispiele BoundedBuffer Implementierung mit Semaphoren
Threadsichere „Apfelernte“ mit Java

13 Bessere Apfelernte Wettlaufsituation Threadsicherer Puffer

14 Semaphore (Funktionsweise)
Semaphore sind eine atomare Operation, die sowohl für das Sperren kritischer Abschnitte, als auch für das Warten auf Bedingungen verwendet werden kann (wird daher auch in BS behandelt). Eine Semaphore (=Ampel) verwaltet eine Anzahl vor “Eintrittskarten” Konzept basiert auf 2 Operationen P (= proberen, testen) und V (= verhogen, erhöhen). P: wenn die Semaphore >0 (Eintrittskarten verfügbar) wird sie erniedrigt, stellt aber keine Barriere dar. Ist sie <= 0, so muss der Thread warten. (häufiger Name: acquire() ) V: die Semaphore wird um 1 erhöht (Eintrittskarte zurückgeben). Wenn dabei der Zähler auf 1 springt, wird ein wartender Thread freigegeben. (häufiger Name: release() ) Dieser Mechanismus heißt zählende Semaphore (Alternative: binäre Semaphore).

15 Semaphore (Einsatzgebiete)
Semaphoren werden oft eingesetzt um zu begrenzen, wie viele Threads auf eine Ressource zugreifen können Ressourcennutzung zu beschränken Semaphoren mit nur einer Eintrittskarte sind (fast) wie Sperren (Locks) Unterschied: die Semaphore kann von anderen Threads freigeben werden (Threads besitzen die Semaphoren nicht!) Eine Semaphore kann als einzige Synchronisationsprimitive verwendet werden. Das sollte man aber nicht tun – Sie ist da sinnvoll, wo der Zugang zu einenm Codeabschnitt von einer bestimmten Anzahl abhängt (s. Beispiel)

16 Synchronisation Bisher betrachtet:
Die Synchronisation von sequentiellen Aufgaben mit Produzent-Verbraucher-Muster mit Bounded Queues und Semaphoren Wie gehen wir mit parallelen Aufgaben um? Wie führen wir Teilergebnisse wieder zusammen? Beispiel Actors: Aufteilen von Listen und wieder zusammenführen

17 Datenparallelität und ForkJoin

18 Phasenbezogene Mechanismen
Vielen nebenläufigen Lösungen ist gemeinsam, dass es immer bestimmte Punkte gibt, an denen die Abläufe mehrerer Threads abgestimmt werden müssen: Ein oder mehrere Threads warten darauf, dass eine Anzahl Threads einen bestimmten Zustand erreicht haben. Synchronisatoren haben Methoden, um diesen Zustand zu ändern, abzufragen und effizient darauf zu warten, dass der Zielstatus erreicht ist (keine Idle-Schleife!) CountDownLatch Zwei Threads synchronisisieren sich an bestimmten Stellen und tauschen dabei Daten aus. Latches warten auf Ereignisse CyclicBarrier Barrieren warten darauf, dass mehrere Threads einen Synchronisationspunkt erreichen, z.B. Simulationen oder Spiele

19 Live Code-Beispiele „Birthday Problem“
Berechnung durch Simulation statt statistischer Formel Viele Berechnungen notwendig Parallele Berechnung für verschiedene Gruppengrößen

20 Latches: Durchgangstore
Verzögerung der Fortführung eines Prozesses bis ein neuer Status erreicht ist Funktioniert wie ein Tor: Solange der Endzustand nicht erreicht ist, ist das Tor geschlossen, niemand kann weiter Wenn Endzustand erreicht ist, dann werden alle Threads weitergelassen Sobald ein Tor offen ist (Endzustand erreicht), kann es nicht wiedergeschlossen werden (es muss ein neuer Latch erzeugt werden) Ziele: Sicherstellen, dass nicht weitergearbeitet wird bis andere (vorhergehende) Aktivitäten vollständig abgeschlossen sind Sicherstellen, dass eine Aktivität nicht startet bevor alle Resourcen initialisiert sind Sicherstellen, dass ein Service nicht startet bevor andere (von diesem Service benötigte) Services gestartet sind Warten bis alle anderen Aktivitäten bereit sind für den nächsten Schritt (z.B. Synchronisation von Simulationen oder Spielen)

21 CountDownLatch CountDownLatch ist eine flexible Implementierung für viele Situationen Erlaubt es einem oder mehreren Threads so lange zu warten bis eine bestimmte Anzahl Events aufgetreten ist (z.B. n Teilergebnisse vorliegen) countDown Methode dekrementiert den Counter wenn das Ereignis eingetreten ist der Zähler ist zu Beginn ungleich 0 await() blockiert bis der Zähler 0 ist (oder Wartezeit abgelaufen oder der wartenende Thread unterbrochen – interrupt – wird) Live Code-Beispiel

22 Futures in Scala Erinnern Sie sich an Actors aus dem Praktikum…
Lösung mit Futures ist einfacher, da geblockt wird bis alle Teilergebnisse da sind (Warten auf das Ergebnis, also blockieren bis alle Ergebnisse da sind) Vorteil: Keine Probleme mit der Reihenfolge der Listen Problem: keine weiteren Ereignisse empfangen

23 Callables und Futures in Java
Erlauben Methodenaufrufe in separaten Threads auszuführen. Oft im Zusammenhang mit weiteren Bibliotheksklassen (Executors, ExecutorService, FutureTask). Problem von Runnables: keine Rückgabewerte -> Lösung Callables Hier landen alle Exceptions im aufrufenden Thread !!

24 Futures in Java Das Verhalten von Future.get() hängt vom Status der Aufgabe ab: - wenn fertig (completed), dann gibt get() das Resultat sofort - sonst blockiert get bis die Aufgabe erledigt ist (completed Status erreicht) Callable kann man sich wie ein Runnable vorstellen, aber mit Result

25 Programme bestehen aus Aufgaben
Fast alle nebenläufigen Anwendungen organisieren die Ausführung von Aufgaben Aufgaben werden nicht nacheinander in einem Thread (z.B. main) ausgeführt sondern nebenläufig Aufteilung in verschiedene, klar gekapselte Aufgaben hat folgende Vorteile: Vereinfachung der Programmorganisation (statt monolithischer Klötze) Fehlerbehebung ist einfacher aufgrund von Transaktiongsgrenzen Ermöglicht Nebenläufigkeit da parallele Bearbeitung einzelner, klar voneinander getrennter Aufgaben möglich ist.

26 Organisation eines Programms in Aufgaben
Erster Schritt: Identifzierung sinnvoll abgegrenzter Aufgaben Ideal: Unabhängige Aufgaben, Arbeit hängt nicht ab von Zustand Rückgabewerten Seiteneffekten Anderen Aufgaben Je unabhängiger, desto nebenläufiger! Unabhängige Aufgaben können immer parallel ausgeführt werden wenn genug Ressourcen vorhanden sind! Kleine Aufgaben verbessern: Flexibilität für den Scheduler Besseres Load Balancing

27 Codebeispiel Webserver Single/Multithread
Responsiveness / Reaktionsfreudigkeit: Wenn ein Request sehr lange blockiert sieht es so aus, als wenn der Service nicht mehr verfügbar ist (down). Passiert z.B. bei intensivem Zugriff auf Datenbanken oder Festplatte CPU ist nicht ausgelastet während auf I/O gewartet wird – dabei könnten schon weitere Anfragen beantwortet werden! Daher besser: mehrere Threads  1. Ansatz: für jede Verbindung ein Thread

28 Codebeispiel Webserver Single/Multithread

29 Paralleler Server Handler sind Threads, die bei Bedarf (geht vom Client aus) vom Server zur Erledigung einer Aufgabe gestartet werden. Sie übernehmen nach dem Start die restliche Kommunikation mit dem Client. Die Handler haben untereinander keine direkte Kommunikation. Sie benutzen oft gemeinsame Objekte (z.B. Datenbank)

30 Konsequenzen der Multithread-Lösung
Aufgaben abarbeiten ist vom main Thread genommen, so dass dieser weiterarbeiten kann Neue Verbindungen können schneller bearbeitet werden Neue Verbindungen können bearbeitet werden, bevor andere Request abgearbeitet sind Aufgaben können parallel bearbeitet werden, mehrer Requests können gleichzeitig bedient werden  Bessere Antwortzeiten und besserer Durchsatz ABER: Taskhandling Code MUSS threadsicher sein! Z.B. durch lokale Objekte. Overhead durch Thread Lifecycle Management

31 Kosten des Multithreadings
Threads verbrauchen zusätzliche Ressourcen, insbesondere Speicher! Stabilität: Es gibt ein Limit wie viele Threads erzeugt werden können (hängt von Parametern der JVM ab, und vom Speicher) Bis zu einem bestimmten Punkt erhöhen mehr Threads auch den Durchsatz. Ab dann verlangsamen weitere Prozesse jedoch die Anwendung! Und wieder gilt: dies mag beim Testen nicht vorkommen, aber bei Livesystemen, die lange laufen… Fazit: Problem beim Single-Thread: Sequentielle Abarbeitung führt zu sehr schlechten Antwortzeiten Problem beim Thread-Per-Task: schlechtes Ressourcenmanagement, Gefahr eines Crashes Daher: Statt Threads selbst erzeugen, lieber auf das Executor-Framework zurückgreifen

32 Executor Interface public interface Executor {
void execute (Runnable command); }

33 Executor Interface Standardlösung für das Entkoppeln von
Tasksubmission und Taskexecution! Aufgaben werden weiterhin als Runnable festgelegt Bonus: ExecutorService erweitert Executor und hat Lifecycle Methoden sowie Hooks für Statistiken, Verwaltung und Monitoring Basiert auf dem Produzent-Verbraucher-Muster Es gibt bereits Standardimplementierung von Executor

34 Einfaches Ändern der Execution Policies

35 Thread Pools Homogener Pool mit Arbeitsthreads
Jeder Arbeitsthread kann (nacheinander) Aufgaben erledigen Es werden nicht ständig neue Threads erzeugt Wenn ein Thread stirbt (z.B. Fehler) kann ein neuer Thread erzeugt werden Aufgaben liegen in einer Warteschlange zum Abarbeiten Ein Arbeitsthread arbeitet einfach: Hole dir die nächste Aufgabe (das nächste Runnable) von der Warteschlange Führe die Aufgabe aus (rufe run() auf) Und so weiter (warte bis wieder Aufgaben in der Warteschlange sind)

36 Fabrikmethoden für Executors
newFixedThreadPool: neue Threads werden erzeugt wenn neue Aufgaben reinkommen bis zu einer maximalen Thread-Anzahl, danach werden Threads wiederverwendet newCachedThreadPool: kein starres Limit sondern Anzahl der Threads wird nach „Bedarf“ angepasst – bei vielen Aufgaben viele Threads, bei wenigen Aufgaben werden Threads aufgegeben newSingleThreadExecutor: Ausführung der Aufgaben in einem einzigen Thread (keine Nebenläufigkeit) entsprechend der Warteschlangenstrategie (FIFO, LIFO, Priorität) newScheduledThreadPool: Thread Pool mit fixierter Größe, mit dem sich verzögerte und periodische Aufgaben organisieren lassen (ähnlich wie Timer, aber fehlerresistenter)

37 Thread Pools - Vorteile
Wiederverwendung von Threads ist kostengünstiger als ständig neue Threads zu erzeugen und freizugeben Weniger Speicherbedarf! Nicht mehr für jede Aufgabe ein zusätzlicher Thread! Durch richtige Konfiguration erhält man immer genug Threads, um die CPU in Arbeit zu halten während man keine Speicherproblem hat MEHR Stabilität! „Degrades Gracefully“ Mehr Möglichkeiten für Tuning, Verwaltung, Monitoring, Logging, Fehlerreporting

38 Executor – Execution Policies
Execution Policy für eine Aufgabengruppe lässt sich leicht ändern. Eine solche Policy legt fest, was, wo, wann und wie etwas ausgeführt wird: In welchem Thread wird die Aufgabe ausgeführt? In welcher Reihenfolge werden Aufgaben erledigt (FIFO, LIFO, Priorisiert)? Wie viele Aufgaben sollen max. nebenläufig bearbeitet werden? Bei Überlastung: welche Aufgaben sollen aussortiert werden und wie wird das System darüber benachrichtigt? Was soll vor/nach Ausführung einer Aufgabe zusätzlich geschehen? Wie werden Aufgaben abgebrochen/gecancelt? Fazit: Statt new Thread(runnable).start() lieber einen Executor dazwischen schalten!!! Das gibt viel mehr Flexibilität.

39 Performance Aufgepasst bei Datenparallelisierung von
hetereogenen Aufgaben: Werden Aufgaben A und B zwischen zwei Workern aufgeteilt, aber A braucht 10x solange wie B, dann ist der Gesamtzuwachs an Geschwindigkeit für den Prozess ist nur 9%. Richtig gute Geschwindigkeitszuwächse erreicht man, wenn man viele unabhängige und homogene Aufgaben hat, die nebenläufig bearbeitet werden können. Richtige Poolgröße: Für rechenintensive (CPU-Nutzung) reicht ein Threadpool von NCPU+1 (selbst rechenintensive Aufgaben pausieren manchmal) Für I/O intensive Aufgaben sollten dagegen mehr Threads erzeugt werden, damit ein anderer Thread die CPU nutzen kann während I/O Operationen ausgeführt werden! Andere Ressourcen: Memory, FileHandler, Socket Handler, Datenbankanbindungen RTask = Wie viele Resourcen braucht jede Aufgabe? RAvailable = Anzahl vorhanden Ressourcen Anzahl sinnvoller Threads = RAvailable / RTask Bsp: wenn ich nur 10 DB-Anbindungen habe, dann bringen auch 20 Threads nicht mehr Datenbankverbindungen

40 Probleme mit Thread Pools
Thread Pools funktionieren am Besten wenn die Aufgaben homogen und unabhängig sind! Das Vermischen von lange und kurzlaufenden Aufgaben führt zum Verklumpen/Verstopfen des Pools: Es kann sein, dass nur noch die lange laufenden Aufgaben am Zug sind und die kurzen Aufgaben ewig in der Warteschleife sind! Nur mit sehr vielen Threads ist dies vermeidbar. Der Pool muss groß genug sein, so dass Aufgaben, die voneinander abhängen, auch alle abgearbeitet werden können ohne abgelehnt oder warten zu müssen. Starvation Deadlocks: Warten auf das Ergebnis anderer Aufgaben, die noch in der Queue sind. Confinement: Aufgaben, die threasicherheit durch confinement herstellen dürfen nur NACHEINANDER ausgeführt werden! Diese Anforderungen sollten DOKUMENTIERT werden, damit nicht später bei der Wartung diese Anforderungen verletzt werden!

41 Performance allgemein
Achtung: viele Techniken zur Performancesteigerung erhöhen die Komplexität Lesbarkeit und Wartbarkeit des Programmcodes verschlechtert sich oft Performancegewinn wird oft überschätzt Tatsächlicher Performancegewinn sollte gemessen und nicht geschätzt werden… Schlechte Konfiguration kann sogar Performanceverlust bedeuten! Bsp: Messen der Unterschiede für Birthday Problem Simulation

42 Skalierbarkeit Fähigkeit die Durchsatzrate bzw. Kapazität zu erhöhen wenn weitere Rechnerressourcen hinzugefügt werden (CPUs, Speicher, Datenspeicher, I/O Bandbreite). Problem: Viele der Tricks, die Performance in einer Single-Threaded-Umgebung zu erhöhen sind problematisch für die Skalierbarkeit. (z.B. Caching, Reordering von Befehlen) Verhältnis sequentieller und paralleler Aufgaben beeinflusst die Skalierbarkeit im Wesenlichen!  Amdahl's Law

43 Amdahl's Law Amdahl wollte seinerzeit zeigen, dass sich Parallelverarbeitung kaum lohnt. Das Gesetz sagt aus, dass die Geschwindigkeitssteigerung durch sequentielle Programmteile entscheidend begrenzt wird. 1 Speedup <= ( 1 – Seq ) N Seq + N = Anzahl Prozessoren Seq = Sequentieller Anteil des Programms Paralleler Anteil = 1 – Seq. Anteil

44 Beispiel: Ein Programm benötigt 20 Stunden auf einem Prozessor 95% lassen sich parallel bearbeiten, aber 1 Stunde (5 %) lässt sich nicht paralellisieren  Dann läuft das Programm mind. 1 Stunde und der maximale Speedup ist 20! Wenn wir einen Rechner mit N Prozessoren ausnutzen wollen, brauchen wir eine hohe Parallelisierbarkeit. Bei vielen Problemen (Simulation physikalischer Prozesse, Big Data) ist die Parallelisierbarkeit beliebig groß! Oft gilt: Sehr rechenintensive Anwendungen = sehr parallele Programme Aber „normale“ Anwendungen sind kaum parallelisierbar !

45 Versteckte Sequentialität
Aufgaben aus der Warteschlange nehmen Warten auf Teilergebnisse und späteres Zusammenführen Warten auf dieselbe Sperre Alle nebenläufigen Anwendungen haben auch Punkte sequentieller Abhängigkeiten! Seqentialität hat negative Auswirkungen auf die Skalierbarkeit

46 Reduzierung des Wettlaufs um Sperren
Neben logischen Abhängigkeiten ist die häufigste Ursache für Serialität das Warten mehrere Threads auf die gleiche Sperre Häufig wird dabei unnötig gewartet, da zu viel gesperrt wird… Wege diesen Wettlauf zu entschärfen: Sperren möglichst kurz nutzen – nur solange wie nötig Häufigkeit der Nutzung von Sperren reduzieren Für unabhängige Daten auch verschiedene Sperren nutzen Ausschließende Sperren durch andere Koordinationsmechanismen ersetzen (z.B. atomare Objekte, Thread Confinement, ReadWriteLocks)

47 Granularität von Sperren
synchronized auf Methoden vermeiden: Es wird ein zu langer Block gesperrt Auch unabhängige Ressourcen werden gesperrt (andere synchronized Methoden können nicht aufgerufen werden, auch wenn diese unabhängige Variablen verändern) Lock Splitting: Verschieden Sperren für voneinander unabhängige Variablen Lock Stripping: Verschiedene Sperren für voneinander unabhängige Datenbereiche (z.B. Arrays nicht mit einer Sperre sondern n Sperren schützen) Nachteil: mehr Sperren erhöhen die Gefahr von Deadlocks aufgrund zirkulärer Abhängigkeiten Implizites Lock Splitting durch delegieren an threadsichere Implementierungen, z.B. threadsichere Set Implementierung

48 Threadsichere Container
Die Containerklassen in java.util sind in der Regel nicht threadsicher (z.B. HashMap)! Man kann aber für viele Fälle ganz einfach ein sicheres Objekt erzeugen: Collections.synchronizedMap Collections.synchronizedList usw. Map<String, Person> m = Collections.synchronizedMap(new HashMap<String, Person>()) Geschützt sind allerdings nur die elementaren Operationen (put, get). Wenn mehrere Operationen zwingend zusammengehören, ist eine entsprechende Synchronisation nötig! Insbesondere muss die Anwendung eines Iterator per synchronized geschützt werden: Set<String> s = m.keySet(); synchronized (m) { for (String x : m) .... Performance Einbußen aufgrund hohen Grads der Serialisierung! Bessere Performance bieten die java.util.concurrent.* Collections, z.B. java.util.concurrent.ConcurrentMap

49 Ausblick Frameworks: Actors für Scala und Java mit akka Play Framework
Servlets Node.js (serverseitiges JavaScript)

50 Kosten des Multithreadings
Koordination zwischen Threads: Sperren, Nachrichtenaustausch, Speichersynchronisation Kontextwechsel kostet Zeit Threads erzeugen und abwickeln Scheduling Overhead Sinnvoles Multithreading bedeutet, dass der Performancezuwachs höher als die Kosten ist Ziel des Multithreadings: Bessere Performance durch Effektivere Nutzung der zur Verfügung stehenden Ressourcen (kein Leerlauf der CPU) Einbinden zusätzlicher Ressourcen wenn diese zur Verfügung stehen (mehrere CPUs auch nutzen) Performance bedeutet: wie schnell und wie viel? Für Serveranwendung ist das „wie viel“ meist wichtiger als das „wie schnell“.


Herunterladen ppt "Paradigmen der Programmierung Nebenläufigkeit"

Ähnliche Präsentationen


Google-Anzeigen