255 lines
7.3 KiB
Markdown
255 lines
7.3 KiB
Markdown
<!--
|
|
title: Nebenläufigkeit
|
|
description: Folien für Nebenläufigkeit in Programmieren 2
|
|
url: https://git.henriburau.de/tutorien/programmieren-2
|
|
header: Programmieren 2 **Tutorium**
|
|
footer: Henri Burau
|
|
-->
|
|
|
|
# Nebenläufigkeit
|
|
|
|
---
|
|
|
|
# Grundlagen
|
|
|
|
Computer führen mehr als eine Aufgabe gleichzeitig aus. Auch innerhalb eine Programms können wir quasi gleichzeitige Ausführung von Programmcode programmieren. Diese Art der Programmierung bezeichnen wir als nebenläufig.
|
|
|
|
Es gibt zwei verschiedene Ausführungseinheiten:
|
|
|
|
* **Prozesse**: Besitzen eigene Laufzeitresourcen, am wichtigsten auch einen eigenen Speicherbereich
|
|
* **Threads**: Sind leichtgewichtige Prozesse. Threads existieren in einem Prozess und teilen sich Laufzeitresourcen mit ihm, wie den Speicherbereich und geöffnete Dateien.
|
|
|
|
Die meisten JVMs laufen in einem einzigen Prozess.
|
|
|
|
---
|
|
|
|
# System Threads
|
|
|
|
Zwei Threads haben wir bereits benutzt. Den main-Thread und den GUI-Thread von JavaFX (`application thread`).
|
|
|
|
---
|
|
|
|
# Deklaration
|
|
|
|
In Java gibt es zwei Möglichkeiten einen neuen Thread zu erzeugen. Entweder man erbt von der Klasse `Thread` oder man implementiert das Interface `Runnable`.
|
|
|
|
---
|
|
|
|
# Von `Thread` erben
|
|
|
|
```java
|
|
class HelloThread extends Thread {
|
|
public void run() {
|
|
System.out.println("Hallo aus einem Thread");
|
|
}
|
|
}
|
|
```
|
|
```java
|
|
Thread thread = new HelloThread();
|
|
thread.start();
|
|
```
|
|
- Simpel: Einfache Implementation
|
|
- Unflexibel: Nebenläufige Klasse muss immer eine Subklasse von `Thread` sein
|
|
|
|
---
|
|
|
|
# `Runnable` implementieren
|
|
```java
|
|
class HelloThread implements Runnable {
|
|
public void run() {
|
|
System.out.println("Hallo aus einem Thread");
|
|
}
|
|
}
|
|
```
|
|
```java
|
|
Thread thread = new Thread(new HelloThread());
|
|
thread.start();
|
|
```
|
|
|
|
- Kompliziert: Etwas schwieriger zu starten
|
|
- Flexibel: Die nebenläufige Klasse kann beliebige Superklassen haben
|
|
|
|
---
|
|
|
|
# Einen Thread pausieren
|
|
|
|
Mithilfe der Methode `Thread.sleep(long milliseconds) throws InterruptedException` kann ein Thread in seiner Ausführung pausiert werden.
|
|
|
|
```java
|
|
class HelloThread implements Runnable {
|
|
public void run() {
|
|
System.out.println("Hallo aus einem Thread");
|
|
|
|
try {
|
|
Thread.sleep(1000);
|
|
} catch (InterruptedException e ) {
|
|
e.printStacktrace();
|
|
}
|
|
|
|
System.out.println("Hallo 1 Sekunde später")
|
|
}
|
|
}
|
|
```
|
|
---
|
|
|
|
# Einen Thread unterbrechen
|
|
|
|
Ein Thread `thread1` kann von einem anderen Thread `thread2` mithilfe der Methode `thread1.interrupt()` unterbrochen werden. Wird ein Thread unterbrochen wird der `Interrupted-State` gesetzt. Der kann mithilfe der Methode `Thread.interrupted()` abgefragt werden. Ist der Thread aktuell pausiert wird die `InterruptedException` der Methode `sleep` geworfen.
|
|
|
|
In beiden Fällen kann der Thread selbst entscheiden ob er abbrechen will.
|
|
|
|
```java
|
|
Thread thread = new Thread(new LongRunningThread());
|
|
thread.start();
|
|
thread.interrupt();
|
|
```
|
|
|
|
---
|
|
|
|
# Auf einen Thread warten
|
|
|
|
Wenn wir aus einem Thread auf die Beendung eines anderen Threads warten wollen, können wir dafür die Methode `join()` nutzen.
|
|
|
|
|
|
```java
|
|
Thread thread = new Thread(new LongRunningThread());
|
|
thread.start();
|
|
System.out.println("Jetzt läuft der Thread");
|
|
thread.join();
|
|
System.out.println("Jetzt ist der Thread fertig");
|
|
```
|
|
---
|
|
|
|
# Lost update problem
|
|
|
|
```java
|
|
class Counter {
|
|
private int c = 0;
|
|
|
|
public void decrement() {
|
|
c--;
|
|
}
|
|
}
|
|
|
|
```
|
|
Intern wird `decrement()` auf jeweils drei Operationen abgebildet:
|
|
|
|
1) Aktuellen Wert von `c` holen
|
|
2) Den ermittelten Wert um 1 dekrementieren
|
|
3) Das Ergebnis in `c` speichern
|
|
|
|
---
|
|
|
|
# Lost update problem
|
|
|
|
Thread `A` und Thread `B` wollen beide den Zähler dekrementieren. Das kann dazu führen das folgendes passiert:
|
|
|
|
Ausgangslage: Das Feld `c` hat den Wert `5`.
|
|
|
|
1) Thread `A`: Aktuellen Wert von `c` holen
|
|
2) Thread `B`: Aktuellen Wert von `c` holen
|
|
3) Thread `A`: Den ermittelten Wert um 1 dekrementieren. Ergebnis ist `4`.
|
|
4) Thread `B`: Den ermittelten Wert um 1 dekrementieren. Ergebnis ist `4`.
|
|
5) Thread `A`: Den ermittelten Wert in `c` abspeichern. `c` ist jetzt `4`.
|
|
6) Thread `B`: Den ermittelten Wert in `c` abspeichern. `c` ist jetzt `4`.
|
|
|
|
---
|
|
|
|
# Mechanismen zur Synchronisation
|
|
|
|
Um das `Lost update problem` zu beheben müssen wir die Threads miteinander synchronisieren. Die Synchronisation kann auf drei verschiedenen Ebenen passieren:
|
|
|
|
* Hardware-Ebene
|
|
* Betriebssystem-Ebene
|
|
* Programiersprachen-Ebene
|
|
|
|
---
|
|
|
|
# HW Ebene
|
|
|
|
Durch den HW-Befehl `SWAP(a,b)`, welcher unteilbar die Werte von zwei Variablen vertauscht, kann eine Synchronisation mit aktivem Warten durchgeführt werden.
|
|
|
|
```java
|
|
int c = 1; //gemeinsame Variable
|
|
int i = 0; //prozesslokale Variable
|
|
|
|
do {
|
|
SWAP(c,i)
|
|
} while (i == 0)
|
|
|
|
// Kritischer Abschnitt
|
|
|
|
SWAP(c,i);
|
|
```
|
|
|
|
Damit können wir schon eine Synchronisation durchführen. Leider basiert die Lösung aber auf aktivem Warten, was nicht perfomant ist.
|
|
|
|
---
|
|
|
|
# Betriebssystem Ebene
|
|
|
|
In einem Betriebssystem können noch andere Prozess-Operationen durchgeführt werden. Es gibt Operationen mit denen ein Prozess freiwillig seine Rechenzeit abgeben kann und es können Prozesse aufgeweckt werden die weiterarbeiten können.
|
|
|
|
Das Betriebssystem hat dabei verschiedene Strategien nach denen der nächste Prozess ausgewählt werden kann.
|
|
|
|
---
|
|
|
|
# Semaphore
|
|
|
|
Semaphore bestehen aus eine Zähler und einer Warteschlange. Bei der Initialisierung eines Semaphores übergibt man den Wert des Zählers, die Warteschlange ist zu Beginn leer.
|
|
|
|
Auf einem Semaphor können zwei Operationen durchgeführt werden:
|
|
|
|
* **P (passieren)**: Der Zähler wird dekrementiert. Ist er positiv, wird der Prozess in den folgenden Abschnitt gelassen. Ist er kleiner als 0 wird der aufrufende Prozess in die Warteschlange eingereiht.
|
|
* **V (freigeben)**: Der Zähler wird inkrementiert. Ist der Zähler jetzt größer oder gleich 0 wird ein Prozess aus der Warteschlange aufgeweckt.
|
|
|
|
So kann die Vergabe von beschränkten Resourcen geregelt werden. Außerdem kann gegenseitiger Ausschluss sichergestellt werden wenn der Sempaphor einen Anfangszähler von 1 hat.
|
|
|
|
---
|
|
|
|
# Monitore
|
|
|
|
Monitore sind ein Bestandteil von Programmiersprachen und ermöglichen gegenseitigen Ausschluss. Idee ist es, dass zusammenhängende Daten **einen** Monitor haben. Im Monitor darf dann immer nur ein Prozess aktiv sein.
|
|
|
|
In Java besitzt jedes Objekt einen eigenen Monitor. Mit dem Schlüsselwort `synchronized` wird signalisiert, dass ein Block von dem Monitor überwacht werden soll. Ist ein Thread in einem solchen Block sorgt der Monitor dafür, dass kein anderer Thread den Block betritt.
|
|
|
|
Diese Art von gegeseitigem Ausschluss ist schnell implementiert, kann aber dazu führen dass mehr synchronisiert wird als nötig.
|
|
|
|
---
|
|
|
|
# Monitor Deklaration
|
|
|
|
Für eine ganze Methode.
|
|
|
|
```java
|
|
public synchronized void ausschlussMethode() {
|
|
// Kritischer Abschnitt
|
|
}
|
|
```
|
|
|
|
Für einen Block.
|
|
|
|
```java
|
|
public void ausschlussBlock() {
|
|
synchronized(this) {
|
|
// Kritischer Abschnitt
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
# Benachrichtigung anderer Threads
|
|
|
|
Innerhalb eines Monitors können Threads miteinander interagieren. Innerhalb eines Monitors können folgende Methoden genutzt werden:
|
|
|
|
* **`wait()`**: Monitor freigeben und in `wait`-Warteschlange warten.
|
|
* **`notify()`**: Einen beliebigen Thread in der `wait`-Warteschlange wecken.
|
|
* **`notifyAll()`**: Alle Threads in `wait`- Warteschlange wecken.
|
|
|
|
---
|
|
|
|
# Komplexität
|
|
|
|
Nebenläufigkeit kann unter Umständen sehr kompliziert werden. Deswegen sollte es nur verwendet werden wenn es wirklich nötig ist.
|