Homepage    Computer-Stoff(Titelseite)   Threads(Titelseite)

Synchronisation von Threads / Mutexe


Das Problem

Damit Threads Daten miteinander austauschen können, müssen sie auf denselben Speicherbereich zugreifen. Hierdurch können leicht unerwünschte Nebenwirkungen entstehen:

/* beispiel2a.c */
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

char ch;

void *print_stern (void *dummy)
{
  ch = '*';
  sleep (1);
  printf ("%c\n", ch);
  return NULL;
}

void *print_minus (void *dummy)
{
  ch = '-';
  sleep (1);
  printf ("%c\n", ch);
  return NULL;
}

int main ()
{
  pthread_t p1, p2;

  pthread_create (&p1, NULL, print_minus, NULL);
  pthread_create (&p2, NULL, print_stern, NULL);

  pthread_join (p1, NULL);
  pthread_join (p2, NULL);

  return 0;
}

Hier sollen die beiden neu gestarteten Threads einfach nur einen Char auf der Standard-Ausgabe ausgeben - der erste ein Minuszeichen, der zweite einen Stern. In Wirklichkeit ist ihr Verhalten jedoch undefiniert. Auf meinem Rechner beispielsweise wurden zwei Sterne ausgegeben.

Der Grund dafür ist einfach: Bevor der erste Thread zur Ausgabe kommt, hat der zweite den Inhalt der globalen Variablen ch überschrieben.

Hier sieht man das Problem: Mehrere Threads bearbeiten gleichzeitig eine statische oder globale Variable und bringen sie dadurch in einen undefinierten Zustand. Bei diesem kleinen Programm ist das Problem natürlich noch sehr einfach zu lösen, aber man stelle sich vor, daß es beispielsweise eine globale Queue von abzuarbeitenden Aufgaben gibt, und ein Thread versucht, sich eine Aufgabe von dort zu holen und diese dann aus der Queue zu entfernen, während ein anderer gerade die Queue umsortiert. Das Ergebnis kann eine korrupte Liste sein, genauso wie ein Speicherzugriffsfehler.


Mutexe

Die Lösung, die man in der Regel anwendet, besteht darin, daß ein Thread, der eine solche globale oder statische Variable bearbeitet, den Zugriff aller anderen Threads darauf blockiert, und, wenn er fertig ist wieder freigibt.

Dies geschieht mit Hilfe sog. "Mutexe" (Singular: Mutex). Ein Thread (Nr.1) kann einen Mutex setzen (to lock a mutex). Jeder anderer Thread (Nr.2), der versucht, diesen Mutex ebenfalls zu setzen, wird solange blockiert, bis Thread Nr.1 ihn wieder freigegeben (unlocked) hat. "Mutex" ist ein Kunstwort, welches an "mutual exclusion" erinnern soll.

In unserem Beispiel sähe das so aus:

/* beispiel2b.c */
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

pthread_mutex_t mutex;
char ch;

void *print_stern (void *dummy)
{
  pthread_mutex_lock (&mutex);
  ch = '*';
  sleep (1);
  printf ("%c\n", ch);
  pthread_mutex_unlock (&mutex);
  return NULL;
}

void *print_minus (void *dummy)
{
  pthread_mutex_lock (&mutex);
  ch = '-';
  sleep (1);
  printf ("%c\n", ch);
  pthread_mutex_unlock (&mutex);
  return NULL;
}

int main ()
{
  pthread_t p1, p2;

  pthread_mutex_init (&mutex, NULL);

  pthread_create (&p1, NULL, print_minus, NULL);
  pthread_create (&p2, NULL, print_stern, NULL);

  pthread_join (p1, NULL);
  pthread_join (p2, NULL);

  return 0;
}

Für eine Erklärung des Programmes s. die beiden nächsten Abschnitte.


Einen Mutex initialisieren

Bevor man einen Mutex benutzen kann, muß man ihn initialisieren:

int pthread_mutex_init (pthread_mutex_t *mutex,
                        const pthread_mutexattr_t *mutexattr);

Die Variable mutex muß dabei auf einen bereits allokierten Speicherbereich zeigen. Im zweiten Argument können Attribute, die die Eigenschaften des Mutex bestimmen, übergeben werden. Hierzu später mehr.

Damit ein Mutex Sinn macht, müssen natürlich alle Threads, die mit diesem Mutex zu tun haben, auf ihn zugreifen können. In der Regel werden Mutexe deshalb globale oder statische Variablen sein.

Manchmal (s. den Abschnitt über einmalig auszuführende Aufgaben) hat man nicht die Möglichkeit, einen Mutex wie oben dynamisch zu initialisieren. Man muß sofort nach dem Programmstart einen funktionsfähigen Mutex, d.h. einen statisch initialisierten Mutex haben. Dies kann man erreichen mit den Makros

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
pthread_mutex_t mutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;

Zu der unterschiedlichen Bedeutung dieser Initializer s. den Abschnitt über Mutex-Attribute! Diese Makros sind implementationsabhängig und ihre Existenz sollte man nicht unbedingt voraussetzen.


Einen Mutex setzen und wieder freigeben

Hierzu gibt es die Funktionen

int pthread_mutex_lock (pthread_mutex_t *mutex);
int pthread_mutex_trylock (pthread_mutex_t *mutex);
int pthread_mutex_unlock (pthread_mutex_t *mutex);

Wird die erste Funktion pthread_mutex_lock aufgerufen, so wird überprüft, ob der Mutex bereits von einem anderen Thread gesetzt wurden. Falls ja, wird so lange gewartet, bis dieser den Mutex wieder freigibt.

Ist der Mutex dagegen frei, so wird er gesetzt, und weitergemacht.

Die Funktion pthread_mutex_trylock dagegen blockiert niemals den Programmfluß: Ist der Mutex gesetzt, so wird der Wert EBUSY zurückgegeben, und sofort weitergemacht. Ist er dagegen frei, so wird er gesetzt, bevor weitergemacht wird.

Mit pthread_mutex_unlock wird ein Mutex schließlich wieder freigegeben. Normalerweise sollte der Thread, der einen Mutex gesetzt hat, diesen auch wieder freigeben. Was passiert, wenn der Mutex von einem anderen Thread gesetzt wurde, hängt von seinen Attributen ab.

Im obigen Programm trifft der erste Thread auf den lock-Befehl. Der Mutex ist noch frei, der Thread setzt ihn und macht weiter. Während er noch im sleep-Befehl verweilt, trifft der zweite Thread auf den lock-Befehl. Das der Mutex nun bereits gesetzt ist, muß der zweite Thread warten, bis der erste fertig ist und den Mutex wieder geunlocked hat. Erst dann geht es weiter.

Mutexe sorgen dafür, daß Programmteile, die durch sie geschützt sind, niemals von mehreren Threads gleichzeitig abgearbeitet werden können. Wenn viele Threads ständig ein und denselben Mutex setzen müssen, werden sie den Großteil ihrer Zeit mit Warten verbringen. I.A. ist es in diesem Fall günstiger, ein Programm mit seriellem Ablauf zu schreiben.

Verwendet man beim Programmieren Mutexe, so sollte man strikt darauf achten, daß jeder Thread, der einen Mutex gesetzt hat, diesen auch wieder freigibt, auch falls Funktionen mit Fehlern oder durch den Empfang eines Signals frühzeitig beendet werden. Nicht freigegebene Mutexe können dazu führen, daß das gesamte Programm blockiert wird und mit C-c oder kill beendet werden muß. Auch die reguläre Beendigung eines Threads mit return und pthread_exit gibt einen gesetzten Mutex nicht wieder frei.


Mutex-Attribute

Was passiert, wenn ein Thread versucht, einen Mutex zu setzen, den er selber schon früher gesetzt hat? Was passiert, wenn ein Thread versucht, einen Mutex freizugeben, den ein anderer Thread gesetzt hat? Nun, dies wird vom aktuell gültigen Mutex-Attribut bestimmt, welches bei der Initialisierung übergeben wird. Bei den LinuxThreads gibt es davon nur drei Stück:

Mutex-Attribut Ergebnis, wenn ein Thread versucht, einen von ihm gesetzten Mutex abermals zu setzen.
PTHREAD_MUTEX_FAST_NP Der Thread blockiert sich selber und bleibt hängen, bis der Prozeß beendet wird. Dies ist die Default-Einstellung, die ein Mutex bekommt, wenn man bei seiner Initialisierung für das Attribut-Argumente NULL übergibt.
PTHREAD_MUTEX_ERRORCHECK_NP pthread_mutex_lock gibt einen Fehler zurück, der Thread läuft aber weiter.
PTHREAD_MUTEX_RECURSIVE_NP Der Thread setzt den Mutex "abermals", d.h. der Mutex muß jetzt mehrmals wieder freigegeben werden, bevor andere Threads nicht mehr blockiert werden. Dies ist manchmal ganz praktisch, wenn man mehrere Funktionen hat, die auf globale Variablen zugreifen, und die sich gegenseitig aufrufen.
Mutex-Attribut Ergebnis, wenn ein Thread versucht, einen von einem anderen Thread gesetzten Mutex freizugeben.
PTHREAD_MUTEX_FAST_NP Der Mutex wird freigegeben.
PTHREAD_MUTEX_ERRORCHECK_NP pthread_mutex_unlock gibt eine Fehlermeldung zurück. Der Zustand des Mutex bleibt unverändert.
PTHREAD_MUTEX_RECURSIVE_NP Der Mutex wird freigegeben.

Mutex-Attribute setzen und auslesen kann man mit dem mehr oder weniger selbsterklärenden Befehlen

int pthread_mutexattr_init (pthread_mutexattr_t *attr);
int pthread_mutexattr_setkind_np (pthread_mutexattr_t *attr, int kind);
int pthread_mutexattr_getkind_np (const pthread_mutexattr_t *attr, int *kind);

wobei die Integer-Variable kind einen der drei obigen Werte hat, und der übergebene Zeiger attr auf einen allokierten Speicherbereich zeigen muß.


Atomare Befehle

Welche statischen Variablen muß man eigentlich durch einen Mutex schützen? Ist es z.B. nötig, das Setzen und Auslesen eines einzigen statischen Bytes durch einen Mutex zu schützen, oder führt der Prozessor und der Speicherbus eine solche Operation immer nur am Stück durch? (Eine solche Operation heißt dann "atomar" und man muß sie nicht durch einen Mutex schützen.

Leider kann die Frage, welche Operationen atomar sind und welche nicht, nicht so einfach beantwortet werden. U.U. kann sogar das Schreiben und Lesen eines einzelnen Bytes nicht-atomar sein. Außerdem hängt die Atomarheit natürlich von der verwendeten Hardware ab. Streng genommen kann man im wesentlichen die Atomarheit keiner einzigen Operation voraussetzen.

Mein Eindruck ist allerdings, daß kleinere Operationen, wie Schreiben oder Lesen von Integer-Zahlen oder Pointern "relativ" atomar sind, was bedeuten soll, daß die Werte solcher Variablen in meinem Programmen bisher nicht durch ungeschützen Zugriff in einen inkonsistenten Zustand geraten sind. Arbeitet man allerdings mit sehr vielen Threads, oder wird eine bestimmte Variable von allen Threads extrem häufig umgesetzt, sollte man sich nicht darauf verlassen, daß alles gut geht.


Bedingungs-Variablen (condition variables)

Manchmal stellt ein Thread fest, daß er nichts zu tun hat, beispielsweise weil eine Queue leer ist. Er sollte so lange warten, bis er wieder etwas zu tun hat. Das Problem ist, daß, um dies festzustellen, er auf durch mit einem Mutex geschützte Daten zugreifen muß. Er muß also den Mutex setzen. Und so lange er den Mutex besetzt hält, kann kein anderer Thread etwas Neues in die Queue schreiben. Eine primitive Lösung bestände also darin, den Thread testen zu lassen, ob etwas für ihn zu tun ist, den Mutex dann freizugeben, und einige Zeit zu warten.

Es gibt jedoch eine bessere Möglichkeit, nämlich eine sog. Bedingungs-Variable (condition variable) zu definieren. Die entsprechenden Befehle sind vor allem folgende:

int pthread_cond_init (pthread_cont_t *cond, pthread_condaddr_t *attr);
int pthread_cond_destroy (pthread_cond_t *cond);

int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait (pthread_cond_t *cond, pthread_mutex_t *mutex, 
                       struct timespec *expiration);

int pthread_cond_signal (pthread_cond_t *cond);
int pthread_cond_broadcast (pthread_cond_t *cond);

Ein Flußplan für den Thread könnte damit ungefähr wie folgt aussehen:

          |
     Mutex setzen
          |            /--------------->  Mutex freigeben
          |           / nein                    |
          |          /                          |
   Ist etwas zu tun?               darauf warten, daß ein anderer 
          |          \         Thread die Bedingungs-Variable umsetzt
          | ja        \                         |
          |            \                        |
    Mutex freigeben     \--------------<   Mutex setzen
          |
          |
  Aufgabe erledigen
          |

Die rechte Seite des Flußplans wird erledigt durch den Befehl pthread_cond_wait. Dieser gibt den Mutex frei, wartet bis ein anderer Thread den signal oder broadcast-Befehl benutzt, und setzt ihn dann wieder.

Der Unterschied zwischen dem signal-und dem broadcast-Befehl ist folgender: Wird der broadcast-Befehl abgesetzt, so erwachen alle Threads, die auf die Bedingungsvariable warten, aus ihrem Schlaf und machen weiter. Im Flußplan oben würden sie alle nochmal testen, ob inzwischen Aufgaben für sie vorliegen. Falls beispielsweise zu wenige Aufgaben für alle Threads vorliegen, würden einige keine mehr bekommen und kämen sofort wieder zu dem pthread_cond_wait.

Der signal-Befehl dagegen weckt nur einen einzigen der wartenden Threads auf. Welchen, kann man nicht festlegen und auch nicht vorhersagen. (Dies ist vermutlich implementationsabhängig.)

Der pthread_cond_timedwait ist genauso wie der pthread_cond_wait-Befehl, nur daß der Thread nicht beliebig lange wartet, sondern maximal eine bestimmte Zeitspanne. Wie man diese festlegt steht in der man-page dieser Befehle. (Ganz unten gibt es ein Beispiel.)


Einmalig auszuführende Aufgaben

Manche Aufgaben, beispielsweise Initialisierungen, müssen einmal, und auch nur genau einmal, ausgeführt werden. Kann man dies nicht am Programmstart machen, sondern müssen Threads die Initialisierung selber vornehmen, beispielsweise weil es sich beim Code um eine Library handelt, so kann man sich mit einem statischen Mutex und einem statischen Flag helfen. Der Flußplan für die Initialisierungs-Routine könnte so aussehen:

           |
      Mutex setzen
           |
           |                 nein
     Flag gesetzt?  ---------------------> Initialisierung vornehmen
           |                                            |
           | ja                                         |
           |                                            |
    Mutex freigeben  <---------------------------  Flag setzen
           |

Das Problem ist, daß auch der Mutex initialisiert werden muß. Man steht hier also vor einem klassischen Henne-Ei-Problem. Lösen läßt sich dieses Problem auf zwei Weisen. Zunächst einmal mit Hilfe der Funktionen

PTHREAD_ONCE_INIT
int pthread_once (pthread_once_t *cntrl, void (*init_routine)(void));

Die statische oder globale Variable cntrl spielt hier die Rolle des Flags. Die Funktion pthread_once sorgt dafür, daß die init_routine nur einmal aufgerufen wird.

Die andere Möglichkeit, welche, sofern sie von der Thread-Implementation, die man benutzt, vorgesehen ist, ist die einer statischen Mutex-Initialisierung (s.o.).


Homepage   Computer-Stoff(Titelseite)   Threads(Titelseite) by Michael Becker, 6/2001 Letzte Änderung: 7/2001.