Sehr Umfangreiche Webseite zum Programmieren in C Perl CGI, Skripting, Linux, Systemprogrammierung C C++ C/C++ ANSI C Linux Linuxsystemprogrammierung Parallele Programmierung Threads Linuxsystemprogrammieren Parallele Programmierung Threads 8. Parallele Programmierung mit Threads

8.1. Unterscheidung zwischen Threads und Prozessen           zurück Ein Kapitel tiefer zum Inhaltsverzeichnis

Parallelprogrammierung mit Threads
Bei manchen Programmieraufgaben kann es nötig sein, dass zwei oder mehrere Aufgaben gleichzeitig erfüllt werden müssen. Man spricht dabei von der Parallelprogrammierung. In parallelen Programmen können Sie Beispielsweise eine Problemstellung in mehreren Teilaufgaben zerlegen, so das diese parallel ausgeführt werden. Natürlich muss man erwähnen, dass Programme nur "wirklich" parallel ablaufen, wenn mindestens zwei Prozessoren vorhanden sind. Aber auch bei Single-CPU's können sich dabei ziemliche Geschwindigkeitsvorteile ergeben.

Unterscheidung zwischen Threads und Prozessen
Sie haben ja schon einiges zu den Prozessen kennen gelernt. Also brauche ich Ihnen dazu nicht mehr allzu viel erklären. Sie haben gesehen wie ein Prozess mit dem Systemaufruf fork() erzeugt und anschließend verwaltet wurde. Mit den IPC's haben Sie gelernt, wie es möglich ist, dass die Prozesse untereinander Daten austauschen. Und genau diese Art der Kommunikation stellen bei Programmierern einen erheblichen Aufwand da, denn man mit Threads nicht mehr betreiben muss.

Außerdem wird bei einer Erstellung eines neuen Prozesses, intern ein erheblicher Aufwand betrieben mit der Duplizierung des gesamten Namespace. Denn im Gegensatz zu den Prozessen, laufen verschiedene Threads in einem gemeinsamen Adressraum ab.

Diese bedeutet, dass das Codesegment, Datensegment der Heap sowie alle anderen Zustandssdaten des Prozesses innerhalb eines Threads jedem Thread zur Verfügung steht. Und daher auch weniger Arbeit beim Austauschen der Daten oder dem Kommunizieren miteinander.

Manch einer wird jetzt bei dem Satz : ...Zustandsdaten des Prozesses innerhalb eines Threads... ins Straucheln geraten sein. Nun die Frage heißt auch Prozesse und Threads, nicht Prozesse oder Threads!

Dazu wollen wir uns einfach den Begriff PROZESSVERWALTUNG etwas genauer ansehen und zerlegen. Die Prozessverwaltung ist Verantwortlich für die gleichzeitige Ausführung mehrerer Prozesse bzw. deren Threads, möglich zu machen und zu koordinieren. Ein Prozess ist also (in der Fachsprache) eine abgeschlossene Verarbeitungseinheit, die einen getrennten Speicherraum und viele andere Ressourcen (siehe Kapitel Prozesse) besitzt.

Ein Prozess besteht aus mindestens einem Thread (jetzt klingelt es langsam). Und ein Prozess endet, wenn alle Threads in den laufenden Threads beendet sind. Alle Ressourcen werde dabei freigegeben.

Im Gegensatz dazu sind Threads unabhängige Befehlsfolgen innerhalb eines Prozesses. Daher teilen sich die Threads auch alles mit dem Prozess in dem Sie laufen. Man könnte auch sagen, Threads sind in einem Prozess gefangen oder verkapselt. Hierzu eine Grafik ...

Threads

Auf diesem Bild könnten nun eine Menge weitere Threads im Prozess laufen. Daran könne Sie erkennen, dass jeder Thread, bis auf ein eigenes Register und einem Teil des Stacks, alles mit dem Prozess und den anderen Threads teilt.

Kernel-Threads und User-Threads
Es gibt zwei Implementationen von Threads. Zum einen die Kernel-Threads und zum anderen der User-Threads. Die User-Threads sind in einer Bibliothek implementiert, die im Speicherbereich des Benutzers ablaufen. Damit ist es möglich, Threads auch auf Betriebssystemen zu verwenden, die keine Threads unterstützen. Der Nachteil an den User-Threads ist aber, dass die einzelnen Threads eines Prozesses, nicht auf Unterschiedlichen Prozessoren bei Multiprozessor-Rechnern laufen. Kernel-Threads hingegen sind bereits im Betriebssystem integrierte Threadsunterstützungen. Dabei wird das Scheduling des Betriebssystem verwendet. So ist es möglich die einzelnen Threads eines Prozesses auf verschiedenen Prozessoren laufen zu lassen.

Kernel-Threads sind mittlerweile in alle Unix-Systemen integriert oder besser gesagt, werden von allen Unix-Systemen unterstützt. Für Linux wird dabei die Library libpthread von Xavier Leroy verwendet, welche eine Implementierung von Kernel-Thread ist.

8.2. Einen Thread erzeugen           zurück Ein Kapitel tiefer Ein Kapitel höher zum Inhaltsverzeichnis

Als erste Funktion dürfte wohl die Funktion zum Starten eines Threads von Interesse sein. Hier der Syntax zum Starten eines neuen Threads ...

#include <pthread.h>

int pthread_create(pthread_t newthread, pthread_attr_t *attribute,
                   void *(*funktion)(void *), void *arg);

Hiermit wird ein neuer Thread erzeugt, mit der Thread-ID newthread, den Attributen attribute (bei Übergabe eines NULL-Zeigers wird die Default-Einstellung genommen), der Funktion funktion, die man genauso sehen kann wie die main()-Funktion bei Prozessen und außerdem noch dem Argumenten arg für die Funktion funktion. Auch hier kann man wenn nicht benötigt den NULL-Zeiger übergeben.

Bevor wir uns ein Programmbeispiel dazu ansehen, benötigen Sie noch eine Funktion um einen oder mehrere Threads wieder zu beenden. Hierzu wird folgende Funktion verwendet ...

#include <pthread.h>

void pthread_exit(void *retval);

Beendet wird der Mainthread auch, falls irgendein beliebiger Thread exit() aufruft oder der Prozess das Signal SIGKILL erhält. Dabei werden alle anderen laufenden Threads ebenfalls beendet.

Zu dem nun folgenden Beispiel müssen Sie die Library pthread hinzulinken mit ...

gcc -o programm programm.c -lpthread

Hier nun das erstes Beispiel zu den Threads ...

#include <stdio.h>
#include <pthread.h>

void hallo(void *name)
{
  int i;
  for(i=0; i<500; i++)
     printf("%d : %s sagt Hallo!\n",i ,(char *)name);
  pthread_exit((void *) 0);
}


int main()
{
 pthread_t t1, t2;

  if(pthread_create(&t1, NULL, (void *)&hallo, (void *)"Thread 1") != 0)
    {
      fprintf(stderr, "Fehler bei Thread 1......\n");
      exit(0);
    }
  if(pthread_create(&t2, NULL, (void *)&hallo, (void *)"Thread 2") != 0)
   {
      fprintf(stderr, "Fehler bei Thread 1......\n");
      exit(0);
    }
  return 0;
}

Wir erzeugen hier zwei Threads und haben schon ein erstes Problem mit dem Programm. Wenn Thread1 bis 500 gezählt hat und sich mit pthread_exit beendet, wird automatisch auch unser Thread2 beendet. Egal wie weit dieser bisher gezählt hat. Es fehlt uns also noch eine Funktion, die auf die Beendigung der einzelnen Threads wartet, wie dies mit wait() oder waitpid() bei fork() erledigt wird.

Hierzu´ die Funktion, die auf die Beendigung eines Threads wartet ...

int pthread_join(phtread_t th, void **retvalue);

Mit dieser Funktion wird auf die Beendigung des Threads th gewartet. Ist der Wert von retvalue ungleich NULL, wird dieser Wert als Rückgabewert des beendeten Threads zurückgegeben. Hierzu nun das Programm mit pthread_join ...

#include <stdio.h>
#include <pthread.h>

void hallo(void *name)
{
  int i;
  for(i=0; i<500; i++)
     printf("%d : %s sagt Hallo!\n",i ,(char *)name);
  pthread_exit((void *) 0);
}


int main()
{
 pthread_t t1, t2;

  if(pthread_create(&t1, NULL, (void *)&hallo, (void *)"Thread 1") != 0)
    {
      fprintf(stderr, "Fehler bei Thread 1......\n");
      exit(0);
    }
  if(pthread_create(&t2, NULL, (void *)&hallo, (void *)"Thread 2") != 0)
   {
      fprintf(stderr, "Fehler bei Thread 1......\n");
      exit(0);
   }
  pthread_join(t1, NULL);
  pthread_join(t2, NULL);

  return 0;
}

Nun wartet auch Thread1 auf die Beendigung von Thread2. Ein weiteres wichtiges Beispiel soll Threads von Prozessen auseinanderhalten. Wenn Sie Beispielsweise die Funktion sleep() verwenden, würde zum Beispiel bei User-Level-Threads der ganze Prozess und somit auch alle anderen Threads lahmgelegt. Daran können Sie erkennen das jeder Thread in einem eigenen Prozess gepackt wird.

Sie können ja gerne mal das nächste Programm testen und dabei eine weitere Konsole öffnen und sich darin mittels ps x übersicht zu den Prozessen zu verschaffen. Und tatsächlich bekommt jeder Thread eine eigenen PID. Dies soll Sie nicht verwirren, dass alle Threads in ein und dem selben Prozess laufen. Hier also das Programmbeispiel mit der Funktion sleep() ...

#include <stdio.h>
#include <pthread.h>

void sleep_t1(void *name)
{
  int i;
  for(i=0; i<5; i++) {
     printf("%s schläft jetzt!\n",(char *)name);
     sleep(3);
     printf("%s ist jetzt wieder wach!\n",(char *)name);
    }
  pthread_exit((void *) 0);
}

void sleep_t2(void *name)
{
  int i;
  for(i=0; i<5; i++) {
     printf("%s schläft jetzt!\n",(char *)name);
     sleep(1);
     printf("%s ist jetzt wieder wach!\n",(char *)name);
    }
  pthread_exit((void *) 0);
}


int main()
{
 pthread_t t1, t2;

  if(pthread_create(&t1, NULL, (void *)&sleep_t1, (void *)"Thread 1") != 0)
    {
      fprintf(stderr, "Fehler bei Thread 1......\n");
      exit(0);
    }
  if(pthread_create(&t2, NULL, (void *)&sleep_t2, (void *)"Thread 2") != 0)
   {
      fprintf(stderr, "Fehler bei Thread 1......\n");
      exit(0);
   }
  pthread_join(t1, NULL);
  pthread_join(t2, NULL);

  return 0;
}

8.3. Attribute von Threads und Scheduling           zurück Ein Kapitel tiefer zum Inhaltsverzeichnis

Die Threads, die Sie eben mit pthread_create erstellt haben, wurden alle mit folgenden Default-Eigenschaften erstellt ...

Diese Eigenschaften hat der Thread also, wenn Sie mit pthread_create als zweites Argument den NULL-Zeiger angeben oder mit der Funktion ...

int pthread_attr_init(pthread_attr_t *attr);

Beispielsweise ...

pthread_t t1;
pthread_attr_t defaultattr;

pthread_attr_init(&defaultattr);
pthread_create(&t1, &defaultattr, (void *)&Funktion, (void *)"Thread" );

Damit würden Sie das selbe erreichen wie mit ...

pthread_create(&t1, NULL, (void *)&Funktion, (void *)"Thread" );

Die Attribute werden in der Regel aber recht selten verändert. Trotzdem will ich Sie für den Fall der Fälle etwas dazu rüsten.

Scheduling
Um die Struktur pthread_attr_t zu verstehen, will ich Ihnen den Scheduler in der Thread-Bibliothek kurz erklären. Den ohne diesem Scheduler würden die Threads machen was sie wollen.

Der Scheduler bestimmt wann welcher Thread Prozesszeit erhält zum Arbeiten. Dies kann mit dem Scheduler nach Priorität oder nach Zeit erfolgen. Nach Priorität laufen die einzelnen Threads vom Thread mit der höchsten bis zur niedrigsten Priorität ab. Bei der Zeit läuft ein Thread eine gewisse Zeit die festgelegt ist und wird dann automatisch unterbrochen.

Da die libpthread-Bibliothek eine Kernel-Level-Bibliothek ist und keine User-Level-Bibliothek stehen uns beide Möglichkeiten zur Verfügung.

Bitte bedenken Sie, wenn sie Threads erstellen die Prioritätsabhängig laufen, dass Sie dafür Verantwortlich sind, wenn ein Thread den Prozessor beansprucht, dieses Blockieren auch wieder aufzuheben, damit andere Threads auch zum Zuge kommen können.

Hier nun die Stuktur worin sich die Eigenschaften von Threads befinden ...

/*Attribute für Threads in  <bits/pthreadstypes.h>*/

typedef struct __pthread_attr_s
     {
      int __detachstate;
      int __schedpolicy;
      struct __sched_param __schedparam;
      int __inheritsched;
      int __scope;
      size_t __guardsize;
      int __stackaddr_set;
      void *__stackaddr;
      size_t __stacksize;
     } pthread_attr_t;

Mit den Funktion ...

int pthread_detach(pthread_t th);

...können Sie die Variable von __detachstate setzen. Dies bedeutet, wenn Sie diese Funktion verwenden, werden alle Resourcen des Threads sofort freigegeben. Auch der Rückgabewert wird nicht mehr benötigt und ist somit mit pthread_join nicht mehr Abzufragen. Sie verlieren dabei allerdings jede Kontrolle des Threads bis zum Ende.

Sie können Threads aber auch direkt detached erzeugen mit der Funktion ...

int pthread_attr_setdetachstate(pthread_attr_t *attr, int datachestat);

Für detachstate können Sie dabei folgende Konstanten verwenden ...

PTHREAD_CREATE_JOINABLE   /*default-Einstellung bei Erzeugung mit pthread_create*/
PTHREAD_CREATE_DETACHED

Im Anwendungsfall sieht dies dann so aus ...

#include <stdio.h>
#include <pthread.h>

void Funktion(void *arg)
{
 printf("Hallo vom %s\n",(char *)arg);
 pthread_exit((void *)0);
}


int main()
{
 pthread_t t1;
 pthread_attr_t attr;

 pthread_attr_init(&attr);
 pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
 pthread_create(&t1, &attr, (void *)&Funktion, (void *)"Thread" );

 return 0;
}

Sie sollten natürlich auch den Fehlerfall überprüfen. Außerdem sollten Sie (ich nehme mal an das Sie nicht nur solch kurzes Snippets erstellen werden) überprüfen, ob Ihr Thread bereits auf detached oder nicht gesetzt wurde. Dies könne sie mit folgender Funktion erledigen ...

int pthread_attr_getdetachstate(pthread_attr_t *attr, int *datachestat);

Den Scheduler des Threads können Sie mit den Funktionen ...

int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
int pthread_attr_getschedpolicy(pthread_attr_t *attr, int policy);

...setzen (set) oder abfragen (get). Folgende Konstanten können Sie dabei an policy vergeben ...

Hierzu natürlich auch wieder ein Anwendungsbeispiel ...

#include <stdio.h>
#include <pthread.h>


void set_sched(void *name)
{
  pthread_attr_t attr;
  int ret;
  int policy;
  policy = SCHED_OTHER;

  ret = pthread_attr_setschedpolicy(&attr, policy);
  if(ret == 0)
     fprintf(stderr,"Konnte Scheduling nicht auf SCHED_OTHER stellen\n");

  printf("Status von %s mit ID %ld : \n",(char *)name,pthread_self());

  pthread_exit((void *) 0);
}

int main()
{
  pthread_t t1, t2;

  if(pthread_create(&t1, NULL, (void *)&set_sched, (void *)"Thread 1") != 0)
   {
     fprintf(stderr, "Fehler bei Thread 1......\n");
     exit(0);
   }
  if(pthread_create(&t2, NULL, (void *)&set_sched, (void *)"Thread 2") != 0)
   {
      fprintf(stderr, "Fehler bei Thread 1......\n");
      exit(0);
   }

  pthread_join(t2, NULL); /*Wir warten bis Thread2 zu Ende ist*/

  return 0;
}

Um den Scheduler nun wirklich zu verändern, also ob nun Zeit-oder-Prioritätsgesteuert gäbe es die Funktionen ...

int pthread_attr_setscope(pthread_attr_t *attr, int scope);
int pthread_attr_getscope(pthread_attr_t *attr, int *scope);

...zum Setzen oder Abfragen des Schedulers. Da Linux-Threads als Konstante für scope nur PTHREAD_SCOPE_SYSTEM unterstützt, ist der Fall trivial. Damit wird der Scheduler Zeitgesteuert. Die Konstante PTHREAD_SCOPE_PROCESS wird von Linux Threads nicht unterstützt für Prioritätsorientierte Steuerung.

Die beiden Funktionen ...

int pthread_attr_setinheritsched(pthread_attr_t *attr, int inherit)
int pthread_attr_getinheritsched(pthread_attr_t *attr, int inherit)

... werden dazu verwendet, zum Setzen oder Abfragen, ob der Thread die Attribute vom Vater-Thread übernimmt. Dazu können Sie der Variablen inherit folgende zwei Konstanten übergeben ...

Manches mal kann es nötig sein, nachzufragen um welchen Thread es sich gerade handelt. Wie bei den Prozessen mit der Funktion getpid(). In diesem Fall können Sie die Funktion ...

pthread_t pthread_self(void);

...verwenden. Hierzu das Anwendungsbeispiel ...

#include <stdio.h>
#include <pthread.h>


void pidof_thread(void *name)
{
  printf("Nachricht von Thread mit der  ID %ld\n",pthread_self());
  pthread_exit((void *) 0);
}

int main()
{
  pthread_t th[3];
  int i;

  for(i=0; i<3; i++)
   {
    if(pthread_create(&th[i], NULL, (void *)&pidof_thread, (void *)NULL) != 0)
      {
       fprintf(stderr, "Fehler beim Erzeugen eines Threads.!?!?!\n");
       exit(0);
      }
   }
  for(i=0; i<3; i++);
    pthread_join(th[i], NULL);

  return 0;
}

Wir erzeugen hier drei Threads und Fragen in der Funktion pidof_thread deren ID's ab. Hier sehen Sie eine andere und komfortablere Möglichkeit Threads zu erzeugen und auf den Beendigungsstatus zu warten.

Es ist auch möglich und manches mal nötig zwei Threads zu vergleichen. Beispielsweise folgende Funktion ...

void what_is_it(void *name)
{
  printf("WauWau\n");
  printf("Miau\n");
  pthread_exit((void *) 0);
}

Sie wollen hier erreichen, dass ein Thread den Hund darstellen soll und ein anderer Thread die Katze. Vergleichen können Sie zwei Threads mit der Funktion ...

int pthread_equal(pthread th1, pthread th2);

Die Funktion liefert ungleich 0 zurück, wenn beide Threads die selbe ID haben. Hier das ganze in unserer Funktion ...

pthread_t th[2];

void what_is_it(void *name)
{
  if(pthread_equal(pthread_self(),th[1]))
    printf("\nThread %ld sagt : WauWau\n",pthread_self());
  if(pthread_equal(pthread_self(),th[0]))
    printf("Thread %ld sagt : Miau\n", pthread_self());
  pthread_exit((void *) 0);
}

Weiter mit 8.4. Threads syncronisieren mit Mutexe