Programmieren_2/nebenlaeufigkeit.md

7.3 KiB

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

class HelloThread extends Thread {
  public void run() {
    System.out.println("Hallo aus einem Thread");
  }
}
Thread thread = new HelloThread();
thread.start();
  • Simpel: Einfache Implementation
  • Unflexibel: Nebenläufige Klasse muss immer eine Subklasse von Thread sein

Runnable implementieren

class HelloThread implements Runnable {
  public void run() {
    System.out.println("Hallo aus einem Thread");
  }
}
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.

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.

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.

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

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.

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.

public synchronized void ausschlussMethode() {
    // Kritischer Abschnitt
}

Für einen Block.

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.