Mutexe verwenden

Ein Mutex ist eine gegenseitige Ausschlusssperre. Nur ein Thread kann die Sperre halten.

Mutexe werden verwendet, um Daten oder andere Ressourcen vor gleichzeitigem Zugriff zu schützen. Ein Mutex hat Attribute, die die Merkmale des Mutex angeben.

Mutex-Attributobjekt

Wie Threads werden Mutexe mithilfe eines Attributobjekts erstellt. Das Mutex-Attributobjekt ist ein abstraktes Objekt, das je nach Implementierung der POSIX -Optionen mehrere Attribute enthält. Der Zugriff erfolgt über eine Variable des Typs pthread_mutexattr_t. Unter AIX® ist der Datentyp pthread_mutexattr_t ein Zeiger; auf anderen Systemen kann es sich um eine Struktur oder einen anderen Datentyp handeln.

Mutex-Attributobjekt erstellen und löschen

Das Mutex-Attributobjekt wird von der Subroutine pthread_mutexattr_init auf Standardwerte initialisiert. Die Attribute werden von Subroutinen verarbeitet. Das Threadattributobjekt wird von der Subroutine pthread_mutexattr_destroy gelöscht. Diese Subroutine kann je nach Implementierung der Threadbibliothek Speicher freigeben, der von der Subroutine pthread_mutexattr_init dynamisch zugeordnet wird.

Im folgenden Beispiel wird ein Mutex-Attributobjekt erstellt und mit Standardwerten initialisiert, dann verwendet und schließlich gelöscht:
pthread_mutexattr_t attributes;
                /* the attributes object is created */
...
if (!pthread_mutexattr_init(&attributes)) {
                /* the attributes object is initialized */
        ...
                /* using the attributes object */
        ...
        pthread_mutexattr_destroy(&attributes);
                /* the attributes object is destroyed */
}

Dasselbe Attributobjekt kann verwendet werden, um mehrere Mutexe zu erstellen. Es kann auch zwischen Mutex-Kreationen geändert werden. Wenn die Mutexes erstellt werden, kann das Attributobjekt gelöscht werden, ohne dass sich dies auf die damit erstellten Mutexes auswirkt.

Mutex-Attribute

Die folgenden Mutex-Attribute sind definiert:

Attribut Beschreibung
Protokoll Gibt das Protokoll an, das verwendet wird, um Prioritätsinversionen für einen Mutex zu verhindern. Dieses Attribut hängt entweder von der Prioritätsübernahme oder von der Option POSIX für den Prioritätsschutz ab.
Prozess-gemeinsam genutzt Gibt die gemeinsame Prozessnutzung eines Mutex an. Dieses Attribut hängt von der Option POSIX für gemeinsame Prozessnutzung ab.

Weitere Informationen zu diesen Attributen finden Sie unter Optionen für Threadbibliothek und Synchronisationsplanung.

Mutexe erstellen und löschen

Ein Mutex wird durch Aufrufen der Subroutine pthread_mutex_init erstellt. Sie können ein Mutex-Attributobjekt angeben. Wenn Sie einen NULL -Zeiger angeben, hat der Mutex die Standardattribute. Daher das folgende Codefragment:
pthread_mutex_t mutex;
pthread_mutexattr_t attr;
...
pthread_mutexattr_init(&attr);
pthread_mutex_init(&mutex, &attr);
pthread_mutexattr_destroy(&attr);
entspricht den folgenden Angaben:
pthread_mutex_t mutex;
...
pthread_mutex_init(&mutex, NULL);

Die ID des erstellten Mutex wird über den Parameter mutex an den aufrufenden Thread zurückgegeben. Die Mutex-ID ist ein nicht transparentes Objekt; ihr Typ ist pthread_mutex_t. In AIXist der Datentyp pthread_mutex_t eine Struktur; auf anderen Systemen kann es sich um einen Zeiger oder einen anderen Datentyp handeln.

Ein Mutex muss einmal erstellt werden. Vermeiden Sie es jedoch, die Subroutine pthread_mutex_init mehrmals mit demselben mutex -Parameter aufzurufen (z. B. in zwei Threads, die gleichzeitig denselben Code ausführen). Die Eindeutigkeit einer Mutex-Erstellung kann wie folgt sichergestellt werden:

  • Aufrufen der Subroutine pthread_mutex_init vor der Erstellung anderer Threads, die diesen Mutex verwenden, z. B. im Anfangsthread.
  • Aufrufen der Subroutine pthread_mutex_init innerhalb einer einzigen Initialisierungsroutine. Weitere Informationen finden Sie unter Einmalinitialisierungen .
  • Verwendung eines statischen Mutex, der vom statischen Initialisierungsmakro PTHREAD_MUTEX_INITIALIZER initialisiert wird; der Mutex hat Standardattribute.
Wenn der Mutex nicht mehr benötigt wird, löschen Sie ihn durch Aufrufen der Subroutine pthread_mutex_destroy . Diese Subroutine kann Speicher freigeben, der von der Subroutine pthread_mutex_init zugeordnet wurde. Nachdem ein Mutex gelöscht wurde, kann dieselbe Variable pthread_mutex_t wiederverwendet werden, um einen weiteren Mutex zu erstellen. Das folgende Codefragment ist beispielsweise gültig, wenn auch nicht sehr praktisch:
pthread_mutex_t mutex;
...
for (i = 0; i < 10; i++) {
 
        /* creates a mutex */
        pthread_mutex_init(&mutex, NULL);
 
        /* uses the mutex */
 
        /* destroys the mutex */
        pthread_mutex_destroy(&mutex);
}

Wie alle Systemressourcen, die von Threads gemeinsam genutzt werden können, muss ein Mutex, der im Stack eines Threads zugeordnet ist, gelöscht werden, bevor der Thread beendet wird. Die Threadbibliothek verwaltet eine verknüpfte Liste von Mutexen. Wenn also der Stack, dem ein Mutex zugeordnet ist, freigegeben wird, wird die Liste beschädigt.

Mutextypen

Der Mutextyp bestimmt, wie sich der Mutex verhält, wenn er bearbeitet wird. Es gibt die folgenden Mutextypen:
PTHREAD_MUTEX_DEFAULT oder PTHREAD_MUTEX_NORMAL
Führt zu einem Deadlock, wenn derselbe pthread versucht, ihn ein zweites Mal mit der Subroutine pthread_mutex_lock zu sperren, ohne ihn zuvor freizugeben. Dies ist der Standardtyp.
PTHREAD_MUTEX_ERRORCHECK
Vermeidet Deadlocks, indem ein Wert ungleich null zurückgegeben wird, wenn derselbe Thread versucht, denselben Mutex mehrmals zu sperren, ohne zuerst den Mutex zu entsperren.
PTHREAD_MUTEX_RECURSIVE
Ermöglicht demselben pthread, den Mutex mit der Subroutine pthread_mutex_lock rekursiv zu sperren, ohne dass dies zu einem Deadlock oder einem Rückgabewert ungleich null von pthread_mutex_lockführt. Derselbe pthread muss die Subroutine pthread_mutex_unlock genauso oft aufrufen wie die Subroutine pthread_mutex_lock , um den Mutex für die Verwendung durch andere pthreads zu entsperren.

Wenn ein Mutex-Attribut zum ersten Mal erstellt wird, hat es den Standardtyp PTHREAD_MUTEX_NORMAL. Nach der Erstellung des Mutex kann der Typ mit dem API-Bibliotheksaufruf pthread_mutexattr_settype geändert werden.

Das folgende Beispiel zeigt die Erstellung und Verwendung eines rekursiven Mutex-Typs:
pthread_mutexattr_t    attr;
pthread_mutex_t         mutex;

pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&mutex, &attr);

struct {
        int a;
        int b;
        int c;
} A;

f()
{
        pthread_mutex_lock(&mutex);
        A.a++;
        g();
        A.c = 0;
        pthread_mutex_unlock(&mutex);
}

g()
{
        pthread_mutex_lock(&mutex);
        A.b += A.a;
        pthread_mutex_unlock(&mutex);
}

Mutexe sperren und entsperren

Ein Mutex ist ein einfaches Schloss mit zwei Zuständen: verriegelt und entriegelt. Beim Erstellen wird ein Mutex entsperrt. Die Subroutine pthread_mutex_lock sperrt den angegebenen Mutex unter den folgenden Bedingungen:

  • Wenn der Mutex entsperrt ist, wird er von der Subroutine gesperrt.
  • Wenn der Mutex bereits durch einen anderen Thread gesperrt ist, blockiert die Subroutine den aufrufenden Thread, bis der Mutex freigegeben wird.
  • Wenn der Mutex bereits durch den aufrufenden Thread gesperrt ist, kann die Subroutine je nach Mutex-Typ unbegrenzt blockieren oder einen Fehler zurückgeben.

Die Subroutine pthread_mutex_trylock verhält sich wie die Subroutine pthread_mutex_lock , ohne den aufrufenden Thread unter den folgenden Bedingungen zu blockieren:

  • Wenn der Mutex entsperrt ist, wird er von der Subroutine gesperrt.
  • Wenn der Mutex bereits durch einen Thread gesperrt ist, gibt die Subroutine einen Fehler zurück.

Der Thread, der einen Mutex gesperrt hat, wird häufig als Eigner des Mutex bezeichnet.

Die Subroutine pthread_mutex_unlock setzt den angegebenen Mutex unter den folgenden Bedingungen auf den entsperrten Status zurück, wenn der aufrufende Mutex Eigner des Mutex ist:

  • Wenn der Mutex bereits entsperrt war, gibt die Subroutine einen Fehler zurück.
  • Wenn der aufrufende Thread Eigner des Mutex war, entsperrt die Subroutine den Mutex.
  • Wenn ein anderer Thread Eigner des Mutex war, gibt die Subroutine je nach Mutex-Typ möglicherweise einen Fehler zurück oder entsperrt den Mutex. Das Entsperren des Mutex wird nicht empfohlen, da Mutexe normalerweise von demselben pthread gesperrt und entsperrt werden.

Da Sperren keinen Abbruchpunkt bereitstellen, kann ein Thread, der blockiert wird, während auf einen Mutex gewartet wird, nicht abgebrochen werden. Es wird daher empfohlen, Mutexe nur für kurze Zeiträume zu verwenden, wie in Fällen, in denen Sie Daten vor gleichzeitigem Zugriff schützen. Weitere Informationen finden Sie unter Abbruchpunkte und Thread abbrechen.

Daten mit Mutexen schützen

Mutexe dienen entweder als Basiselement der unteren Ebene, von dem aus andere Threadsynchronisationsfunktionen erstellt werden können, oder als Datenschutzsperre. Weitere Informationen zum Implementieren von langen Sperren und Writer-Priority-Reader-/Writer-Sperren finden Sie unter Mutexes verwenden.

Mutex-Verwendungsbeispiel

Mutexe können verwendet werden, um Daten vor gleichzeitigem Zugriff zu schützen. Eine Datenbankanwendung kann beispielsweise mehrere Threads erstellen, um mehrere Anforderungen gleichzeitig zu verarbeiten. Die Datenbank selbst wird durch einen Mutex namens db_mutexgeschützt. Beispiel:
/* the initial thread */
pthread_mutex_t mutex;
int i;
...
pthread_mutex_init(&mutex, NULL);    /* creates the mutex      */
for (i = 0; i < num_req; i++)        /* loop to create threads */
        pthread_create(th + i, NULL, rtn, &mutex);
...                                  /* waits end of session   */
pthread_mutex_destroy(&mutex);       /* destroys the mutex     */
...

/* the request handling thread */
...                                  /* waits for a request  */
pthread_mutex_lock(&db_mutex);       /* locks the database   */
...                                  /* handles the request  */
pthread_mutex_unlock(&db_mutex);     /* unlocks the database */
...

Der Anfangsthread erstellt den Mutex und alle Anforderungsverarbeitungsthreads. Der Mutex wird mit dem Parameter der Eingangspunktroutine des Threads an den Thread übergeben. In einem realen Programm kann die Adresse des Mutex ein Feld einer komplexeren Datenstruktur sein, die an den erstellten Thread übergeben wird.

Deadlocks vermeiden

Es gibt eine Reihe von Möglichkeiten, wie eine Multithread-Anwendung einen Deadlock verursachen kann. Hier einige Beispiele:
  • Ein mit dem Standardtyp PTHREAD_MUTEX_NORMALerstellter Mutex kann nicht von demselben pthread erneut gesperrt werden, ohne dass es zu einem Deadlock kommt.
  • Eine Anwendung kann einen Deadlock verursachen, wenn Mutexe in umgekehrter Reihenfolge gesperrt werden. Das folgende Codefragment kann beispielsweise einen Deadlock zwischen den Threads A und B erzeugen.
    /* Thread A */
    pthread_mutex_lock(&mutex1);
    pthread_mutex_lock(&mutex2);
    
    /* Thread B */
    pthread_mutex_lock(&mutex2);
    pthread_mutex_lock(&mutex1);
  • Eine Anwendung kann in einem so genannten Ressourcen -Deadlock einen Deadlock verursachen. Beispiel:
    struct {
                    pthread_mutex_t mutex;
                    char *buf;
            } A;
    
    struct {
                    pthread_mutex_t mutex;
                    char *buf;
            } B;
    
    struct {
                    pthread_mutex_t mutex;
                    char *buf;
            } C;
    
    use_all_buffers()
    {
            pthread_mutex_lock(&A.mutex);
            /* use buffer A */
    
            pthread_mutex_lock(&B.mutex);
            /* use buffers B */
    
            pthread_mutex_lock(&C.mutex);
            /* use buffer C */
    
            /* All done */
            pthread_mutex_unlock(&C.mutex);
            pthread_mutex_unlock(&B.mutex);
            pthread_mutex_unlock(&A.mutex);
    }
    
    use_buffer_a()
    {
            pthread_mutex_lock(&A.mutex);
            /* use buffer A */
            pthread_mutex_unlock(&A.mutex);
    }
    
    functionB()
    {
            pthread_mutex_lock(&B.mutex);
            /* use buffer B */
            if (..some condition)
            { 
              use_buffer_a();
            }
            pthread_mutex_unlock(&B.mutex);
    }
    
    /* Thread A */
    use_all_buffers();
    
    /* Thread B */
    functionB();
    Diese Anwendung hat zwei Threads: thread A und thread B. Thread B beginnt zuerst mit der Ausführung, dann startet thread A kurz danach. Wenn thread A use_all_buffers () ausführt und A.mutexerfolgreich sperrt, blockiert es, wenn es versucht, B.mutexzu sperren, da thread B es bereits gesperrt hat. Während thread B functionB ausführt und some_condition auftritt, während thread A blockiert ist, blockiert thread B jetzt auch den Versuch, A.mutexanzufordern, das bereits von thread Agesperrt ist. Dies führt zu einem Deadlock.

    Die Lösung für diesen Deadlock besteht darin, dass jeder Thread alle erforderlichen Ressourcensperren anfordert, bevor die Ressourcen verwendet werden. Wenn er die Sperren nicht anfordern kann, muss er sie freigeben und erneut starten.

Mutexes und Konkurrenzsituationen

Gegenseitige Ausschlusssperren (Mutexes) können Dateninkonsistenzen aufgrund von Konkurrenzsituationen verhindern. Eine Konkurrenzsituation tritt häufig auf, wenn zwei oder mehr Threads Operationen für denselben Speicherbereich ausführen müssen, die Ergebnisse der Berechnungen jedoch von der Reihenfolge abhängen, in der diese Operationen ausgeführt werden.

Angenommen, ein einzelner Zähler, X, wird um zwei Threads, A und B, erhöht. Wenn X ursprünglich 1 ist, erhöhen die Zeitthreads A und B den Zähler, X sollte 3 sein. Beide Threads sind unabhängige Entitäten und haben keine Synchronisation zwischen ihnen. Obwohl die C-AnweisungX++Sieht einfach genug aus, um atomar zu sein, ist der generierte Assemblierungscode möglicherweise nicht, wie im folgenden Pseudo-Assembler-Code gezeigt:
move    X, REG
inc     REG
move    REG, X

Wenn beide Threads im vorherigen Beispiel gleichzeitig auf zwei CPUs ausgeführt werden oder wenn durch die Zeitplanung die Threads alternativ für jede Anweisung ausgeführt werden, können die folgenden Schritte auftreten:

  1. Thread A führt die erste Anweisung aus und stellt X(1) in das Register von Thread A. Anschließend wird Thread B ausgeführt und stellt X(1) in das Register von Thread B. Das folgende Beispiel veranschaulicht die resultierenden Register und den Inhalt des Speichers X.
    Thread A Register = 1
    Thread B Register = 1
    Memory X          = 1
  2. Thread A führt die zweite Anweisung aus und erhöht den Inhalt seines Registers auf 2. Anschließend erhöht Thread B sein Register auf 2. Es wird nichts in den Speicher Xverschoben, sodass der Speicher X unverändert bleibt. Das folgende Beispiel veranschaulicht die resultierenden Register und den Inhalt des Speichers X.
    Thread A Register = 2
    Thread B Register = 2
    Memory X          = 1
  3. Thread A verschiebt den Inhalt seines Registers (jetzt 2) in den Speicher X. Anschließend verschiebt Thread B den Inhalt seines Registers, das ebenfalls 2 ist, in den Speicher Xund überschreibt den Wert von Thread A. Das folgende Beispiel veranschaulicht die resultierenden Register und den Inhalt des Speichers X.
    Thread A Register = 2
    Thread B Register = 2
    Memory X          = 2

In den meisten Fällen führen Thread A und Thread B die drei Anweisungen nacheinander aus und das Ergebnis wäre wie erwartet 3. Rassenbedingungen sind in der Regel schwer zu entdecken, weil sie sporadisch auftreten.

Um diese Konkurrenzsituation zu vermeiden, sollte jeder Thread die Daten sperren, bevor er auf den Zähler zugreift und den Speicher Xaktualisiert. Wenn Thread A beispielsweise eine Sperre übernimmt und den Zähler aktualisiert, verlässt er den Speicher X mit dem Wert 2. Nachdem Thread A die Sperre freigegeben hat, nimmt Thread B die Sperre und aktualisiert den Zähler, wobei er 2 als Anfangswert für X nimmt und den Wert auf 3 erhöht, das erwartete Ergebnis.