Home
 

Spezielle Anwendersprachen -- Threads++

Threads++ Logo

Überblick

Das Referat wurde in 3 Teile aufgeteilt, wobei Teil 2 von mir stammt. Hier sind Teil1 und Teil 3 zu finden.

Einstimmung

Im folgenden werden einige Sprachkonstrukte von Ada, Java und Threads++ miteinander anhand von Beispielen vergleichen. Für eine ausführliche Beschreibung möchte ich auf die entsprechenden Sprachdokumentationen verweisen.

Basis

Zunächst dürfte von Bedeutung sein wie man einen Thread erzeugen und starten kann.

In Ada, wo die Threads Tasks heißen, geht das mit folgender Konstruktion:

with Stringpack; use Stringpack;
procedure Tierwelt is
   task Hund;
   task body Hund is
   begin
      Print("wau");
      Print("wau");
   end Hund;
begin
   null;
end Tierwelt;

Der Thread wird automatisch bei Beginn des Hauptprogrammes gestartet.

In Java gibt es eine Basisklasse java.lang.Thread, die einen fertigen Thread darstellt; allerdings ohne jegliche Funktion. Dazu muß man die Methode run überschreiben:

public class Hund extends Thread {
   public void run() {
        System.out.println("wau");
        System.out.println("wau");
   }

   public static void main(String argv[]) {
      Hund h = new Hund();
      h.start();
   }
}

Zu beachten ist hier daß der Thread nicht sofort losläft, sondern explizit über start gestartet werden muß.
Eine Alternativmethode stellt das Runnable-Interface dar:

public class Hund implements Runnable {
   public void run() {
      System.out.println("wau");
      System.out.println("wau");
   }

   public static void main(String argv[]) {
      Thread h = new Thread(Hund);
      h.start();
   }
}

Und nun zu Threads++:

#include <stdio.h>
#include <Thread.hh>

void *Hund(void *) {
   printf("wau\n");
   printf("wau\n");

   return NULL;
}

Thread t(Hund, NULL);
void main(int argc, char *argv[]) {
   t.join();
}

Bei Ada und Java besitzen die Threads ein ziemlich unabhängiges Eigenleben; bei Threads++ sind sie vom Hauptprogramm abhängig und werden sofort beendet, wenn das Hauptprogramm beendet wird. Wie bei Ada laufen auch hier die Threads sofort los.

Die Java Thread-Klasse und das Runnable-Interface kann man recht einfach in C++ nachbilden. Zuerst braucht man das Interface selbst:

class Runnable {
public:
   virtual void *run() {}
};

Dann braucht man nur noch ein Thread-Klasse, in der die run-Methode verpackt wird bzw. ein Runnable verwendet wird:

#include <Thread.hh>

class RunnableThread: public Thread, public Runnable {
public:
   RunnableThread();
   RunnableThread(Runnable *);

private:
   static void *runWrapper(void *);
   static void *runWrapperRunnable(void *);
};


RunnableThread::RunnableThread() : Thread(runWrapper, (void *) this) {}

RunnableThread::RunnableThread(Runnable *r) : Thread(runWrapperRunnable,
                                                     (void *) r) {}

void *RunnableThread::runWrapperRunnable(void *r) {
   Runnable *myR = (Runnable *)r;

   return myR->run();
}

void *RunnableThread::runWrapper(void *t) {
   RunnableThread *myT = (RunnableThread *)t;

   return myT->run();
}

Folgendes Beispiel zeigt nun beide Anwendungsmöglichkeiten:

class Hund: public RunnableThread
{
 public:
  Hund(): RunnableThread() {}

 protected:
  void *run();
};

void *Hund::run()
{
  cout << "wau" << endl;
  cout << "wau" << endl;

  return NULL;
}


class Katz: public Runnable
{
 public:
  Katz(): Runnable() {}

 protected:
  void *run();
};

void *Katz::run()
{
  cout << "miau" << endl;

  return NULL;
}



int main(int argc, char *argv[])
{
  Hund myHund;
  Katz katzRun;
  RunnableThread myKatz(&katzRun);

  cout << "Hello world" << endl;

  myHund.join();
  myKatz.join();
}

Wechselseitiger Ausschluß

In Ada gibt es meines Wissens nach keine direkte Unterstützung für wechselseitigen Ausschluß (kein großes Problem da leicht zu implementieren, siehe Vorlesung). In Java gibt es die Synchronized-Blöcke auf beliebigen Objekten:

Object mutex;

void f() {
   ...
   synchronized(mutex) {
      // critical section
   }
}

Threads++ bietet direkt eine Mutex-Klasse an:

#include <Mutex.hh>

Mutex mutex;

void f() {
   ...
   mutex.lock();
   // critical section
   mutex.unlock();
}

Eine interessante Alternative zum händischen lock/unlock stellt die Lock-Klasse dar, die den Existenzbereich einer Variable ausnützt und möglicherweise hilft Fehler bei der Freigabe zu vermeiden, insbesondere wenn ganze Funktionen gesperrt werden:

#include <Mutex.hh>

Mutex mutex;

void f() {
   ...
   {
      Lock lock(mutex);
      // critical function
   }
}

Entries

Weder Java noch Threads++ bieten den Luxus eines entries. Ich möchte hier nur die einfachste Version anführen, eine Erweiterung ist bei entsprechender Sorgfalt sicher nicht schwierig.

#include <iostream.h>
#include <Thread.hh>
#include <Mutex.hh>
#include <Condition.hh>

#include <RunnableThread.h>

class Hund : public RunnableThread {
public:
  Hund() : RunnableThread(), accept(NONE), caller(mutex), callee(mutex) {}
 
  void *run();

  void entry_los();
  
 private:
  enum entries {NONE, LOS};

  entries accept;

  Mutex mutex;
  Condition caller, callee;
};


void Hund::entry_los() {
  Lock lock(mutex);

  while (accept != LOS) {
    caller.wait();
  }

  // hand over parameter or do whatever exclusive

  accept = NONE;
  callee.signal();
}

void *Hund::run() {
  {
    Lock lock(mutex);
    accept = LOS;
    caller.signal();
    while (accept != NONE) {
      callee.wait();
    }
  }
  
  cout << "wau 1" << endl;
  cout << "wau 2" << endl;
}

Hund rex, pluto, fifi;


class Katz : public RunnableThread {
public:
  Katz() : RunnableThread(), accept(NONE), caller(mutex), callee(mutex) {}
  
  void *run();
  
  void entry_los();
  void entry_nochmal();
  
private:
  enum entries {NONE, LOS, NOCHMAL};
  
  entries accept;
  
  Mutex mutex;
  Condition caller, callee;
};

void Katz::entry_los() {
  Lock lock(mutex);

  while (accept != LOS) {
    caller.wait();
  }

  // hand over parameter or do whatever exclusive

  accept = NONE;
  callee.signal();
}

void Katz::entry_nochmal() {
  Lock lock(mutex);

  while (accept != NOCHMAL) {
    caller.wait();
  }

  // hand over parameter or do whatever exclusive

  accept = NONE;
  callee.signal();
}

void *Katz::run() {
  {
    Lock lock(mutex);
    accept = LOS;
    caller.signal();
    while (accept != NONE) {
      callee.wait();
    }
  }
  
  cout << "miau 1" << endl;
  
  {
    Lock lock(mutex);
    accept = NOCHMAL;
    caller.signal();
    while (accept != NONE) {
      callee.wait();
    }
  }
  
  cout << "miau 2" << endl;
}

Katz katz;

void main(char *argv[]) {
  cout << "hello world" << endl;

  rex.entry_los();
  fifi.entry_los();
  katz.entry_los();
  pluto.entry_los();
  katz.entry_nochmal();

  rex.join();
  fifi.join();
  katz.join();
  pluto.join();
}

Die Java-Variante sieht analog dazu aus, wobei die Conditions durch die Java-Konstrukte ersetzt werden müssen; diese sind Grundlage eines jeden Objekts. Die Wait-Methode heißt in Java auch wait, signal heißt dort aber notify. Das Ganze gehöhrt dann noch in den üblichen Synchronized-Try-Block, also etwa so:

synchronized (mutex) {
   try { wait(); } catch (InterruptedException e) {}
   ...
   notify();
}