2. Thread-Programmierung


Thread Funktionalität als Sprachkonstrukte oder externe Bibliotheken

Abbildung 1: Threads innerhalb von Prozessen

\begin{picture}(142.00,145.00)
% put(21.00,22.00)\{ framebox(121.00,123.00)[cc]\...
...(118.00,126.00){\circle*{5.20}}
\put(30.00,140.00){\circle*{5.20}}
\end{picture}

Abbildung 1a: Thread Zustände

Thread States

Java-Implementierung von Threads, enthalten in dem Java-Package java.lang:

  1. der Klasse Thread und dem Interface Runnable,
  2. dem Java-Sprachkonstrukt synchronized und
  3. den Methoden wait(), notify() der Basisklasse Object.

2.1 Thread Erzeugung

Erzeugung von Threads:
ein Objekt einer geeigneten Klasse erzeugen,
Aufruf einer Methode dieses Objekts.

Für die Klasse gibt es zwei Möglichkeiten:

Subklasse von Thread

Implementierung des Runnable-Interface

Der Thread-Konstruktor und die Thread-Methoden start() und join() haben die folgenden Spezifikationen.

   public Thread(Runnable target)
   public Thread(Runnable target, String name)
   public Thread(ThreadGroup group, Runnable target, String name)
   public synchronized void start()
   public final void join() throws InterruptedException

erster Eindruck eines parallelen Java-Programms:

      class Action implements Runnable {
         int var;
         public Action(int v) { var = v; }
         public void run() { doSomeWork(var);  }
      }
      Thread t1 = new Thread(new Action(1));
      Thread t2 = new Thread(new Action(2));
      Thread t3 = new Thread(new Action(3));
      try {
          t1.start(); t2.start(); t3.start();
          t1.join(); t2.join(); t3.join();
      }
      catch (InterruptedException e) { ... }

2.2 Synchronisation von kritischen Bereichen

In verschiedenen run()-Methoden u.U. gleichzeitiger Zugriff auf globale Variablen.

Wir können nicht verhindern, daß Schreib- oder Lese-Operationen auf den globalen Speicher in nebenläufigen Prozessen stattfinden und sichtbar werden.

Das Java-Sprachkonstrukt synchronized hat die folgenden Varianten.

      synchronized (object) { ... }
      synchronized (static object) { ... }
      synchronized type methodName(...) { ... }
      static synchronized type methodName(...) { ... }

Die Semantik von synchronized type methodName(...) { S1; ...; Sn; }
entspricht

      type methodName(...) {
           synchronized(this) { S1; ...; Sn; }
      }

Bemerkungen:

2.3 Warten auf Bedingungen

Problem: Initialisierung innerhalb eines parallelen Ablaufs.
Beispiel: Summe von Vektoren.

Zur Verfügung stehen uns die Object-Funktionen wait() und notify() mit den folgenden Spezifikationen:

    public final void wait() throws InterruptedException
    public final void wait(long timeout) 
                           throws InterruptedException
    public final void notify()
    public final void notifyAll()

wait() nur innerhalb eines synchronized Abschnitts

Fall eines beliebigen Booleschen Ausdrucks der erfüllt sein soll.

2.4 Zusammenfassung

2.5 Beispiele

 

ExCon

Abbildung A1:ExCon

 

UML ExCon

Abbildung A2:UML Seq ExCon


 

ExAtom

Abbildung B1:ExAtom

 

UML Seq ExAtom

Abbildung B2:UML Seq ExAtom


2.6 Semaphore

 

Es gibt zwei Operationen `V' (Abkürzung für holländisch `frei') und `P' (für `passieren') auf Semaphoren `sem':

sem.P():
entspricht dem Eintritt (Passieren) in einen synchronisierten Bereich, wobei mitgezählt wird, der wievielte Eintritt es ist.
sem.V():
entspricht dem Verlassen (Freigeben) eines synchronisierten Bereiches, wobei ebenfalls mitgezählt wird, wie oft der Bereich verlassen wird.
Es wird sichergestellt, daß die Anzahl der Eintritte (#P) kleiner oder gleich der Anzahl der Austritte (#V) plus ein Initialisierungswert ist

# P <= # V + init.

Das Zählen der P- und V-Operationen kann mit einer Variablen

s = # V + init - # P
und der Bedingung s >= 0 erledigt werden.

Implementierungsskizze:

       sem.P(): synchronized (mux) {
                  while (s <= 0) { "waiting = true"
                        wait(); }
                  s--;
               }

       sem.V(): synchronized (mux) {
               s++;
               if ("some are waiting") { notify(); }
               }

Implementierungen: Sema.java, Semaphore.java

 

Sema

Abbildung C1:UML Semaphore


2.7 Barrieren

Mit Barrieren kann man warten, bis sich eine vorgegebene Anzahl von Teilnehmern angemeldet hat, und dann weiterarbeiten.

     public class Barrier {
     
       private int n, b;
     
       public Barrier(int i) {
         n = i; b = 0;
       }
     
       public synchronized void check() {
         b++;
         if (b < n) {       
            try { this.wait();
            } catch (InterruptedException e) {}
         }
         else { b = 0; 
            this.notifyAll();
         }
       }
     }

 

Barrier

Abbildung D1:UML Barrier


      public class Barrier {
     
        private int n, b;
        private Sema bs, bl;
     
        public Barrier(int i) {
          n = i; b = 0;
          bs = new Sema(0);
          bl = new Sema(1);
        }
     
        public void check() {
          bl.P();
          b++;
          if (b < n) { 
             bl.V(); bs.P(); 
          }
          else {
             b = 0; 
             for (int j=1; j<n; j++) { bs.V(); }
             bl.V();
          }
        }   
      }

2.8 BoundedBuffer

 

n Produzenten erzeugen Daten, die an m Konsumenten weitergegeben werden sollen. Zur Weitergabe der Daten dient ein Puffer, der in der Lage sein soll, k Daten zu speichern ( 1 <= n, 1 <= m, 1 <= k ). Falls der Puffer voll ist, sollen die Produzenten warten, bis wieder Platz ist, und falls der Puffer leer ist, sollen die Konsumenten warten, bis wieder Daten vorhanden sind. Falls keine Arbeit mehr anliegt, sollen die Produzenten und die Konsumenten terminieren.

Der Puffer wird als Ringpuffer implementiert, siehe Abbildung E1. Die Variablen `front' und `rear' zeigen jeweils auf den nächsten vollen Platz zum Lesen des Puffers bzw. auf den nächsten freien Platz zum Schreiben. Falls der Puffer nicht voll ist, kann ein Produzent in den Puffer schreiben, und falls der Puffer nicht leer ist, kann ein Konsument aus dem Puffer lesen.

Die Synchronisation muß somit sicherstellen, daß der Produzent wartet, falls der Puffer voll ist, und der Konsument wartet, falls der Puffer leer ist. Das heißt, sei x die Anzahl der Elemente im Puffer, dann muß immer gelten 1 <= x <= k. Insbesondere muß gelten x <= k damit der Produzent eine Nachricht schreiben darf. Und es muß gelten 1 <= x damit der Konsument eine Nachricht lesen kann.

 

Ringpuffer

Abbildung E1:Ringpuffer

Implementierung: BoundedBuffer.java


© Universität Mannheim, Rechenzentrum, 2000-2009.

Last modified: Wed Sep 12 21:28:54 CEST 2009