Threadspezifische Daten

Viele Anwendungen erfordern, dass bestimmte Daten auf Threadbasis über Funktionsaufrufe hinweg verwaltet werden.

Beispielsweise muss ein Multithread-Befehl grep , der einen Thread für jede Datei verwendet, threadspezifische Dateihandler und eine Liste gefundener Zeichenfolgen enthalten. Die threadspezifische Datenschnittstelle wird von der Threadbibliothek bereitgestellt, um diese Anforderungen zu erfüllen.

Threadspezifische Daten können als zweidimensionales Array von Werten angezeigt werden, wobei Schlüssel als Zeilenindex und Thread-IDs als Spaltenindex dienen. Ein threadspezifischer Datenschlüssel ist ein nicht transparentes Objekt des Datentyps pthread_key_t . Derselbe Schlüssel kann von allen Threads in einem Prozess verwendet werden. Obwohl alle Threads denselben Schlüssel verwenden, legen sie unterschiedliche threadspezifische Datenwerte für diesen Schlüssel fest und greifen darauf zu. Threadspezifische Daten sind leere Zeiger, die es ermöglichen, auf jede Art von Daten zu verweisen, z. B. dynamisch zugeordnete Zeichenfolgen oder Strukturen.

In der folgenden Abbildung hat Thread T2 den threadspezifischen Datenwert 12, der dem Schlüssel K3zugeordnet ist. Thread T4 hat den Wert 2, der demselben Schlüssel zugeordnet ist.
Keys T1 Gewinde T2 Thread T3 Gewinde T4 Thread
K1 6 56 4 1
K2 87 21 0 9
K3 23 12 61 2.
K4 11 76 47 88

Schlüssel erstellen und löschen

Threadspezifische Datenschlüssel müssen erstellt werden, bevor sie verwendet werden. Ihre Werte können automatisch gelöscht werden, wenn die entsprechenden Threads beendet werden. Ein Schlüssel kann auch auf Anforderung gelöscht werden, um seinen Speicher zurückzufordern.

Schlüsselerstellung

Ein threadspezifischer Datenschlüssel wird durch Aufrufen der Subroutine pthread_key_create erstellt. Diese Subroutine gibt einen Schlüssel zurück. Die threadspezifischen Daten werden für alle Threads, einschließlich der noch nicht erstellten Threads, auf den Wert NULL gesetzt.

Betrachten Sie zum Beispiel zwei Threads A und B. Thread A führt die folgenden Operationen in chronologischer Reihenfolge aus:

  1. Erstellen Sie einen threadspezifischen Datenschlüssel K.

    Threads A und B können den Schlüssel Kverwenden. Der Wert für beide Threads ist NULL.

  2. Erstellen Sie einen Thread C.

    Thread C kann auch den Schlüssel Kverwenden. Der Wert für Thread C ist NULL.

Die Anzahl der threadspezifischen Datenschlüssel ist auf 450 pro Prozess begrenzt. Diese Zahl kann durch die symbolische Konstante PThREAD_KEYS_MAX abgerufen werden.

Die Subroutine pthread_key_create darf nur einmal aufgerufen werden. Andernfalls werden zwei verschiedene Schlüssel erstellt. Betrachten Sie zum Beispiel das folgende Codefragment:

/* a global variable */
static pthread_key_t theKey;
 
/* thread A */
...
pthread_key_create(&theKey, NULL);   /* call 1 */
...
 
/* thread B */
...
pthread_key_create(&theKey, NULL);   /* call 2 */
...

In unserem Beispiel werden die Threads A und B gleichzeitig ausgeführt, aber Aufruf 1 erfolgt vor Aufruf 2. Aufruf 1 erstellt einen Schlüssel K1 und speichert ihn in der Variablen theKey . Aufruf 2 erstellt einen weiteren Schlüssel K2und speichert ihn auch in der Variablen theKey , wodurch K1überschrieben wird. Als Ergebnis verwendet Thread A K2, vorausgesetzt es ist K1. Diese Situation sollte aus folgenden Gründen vermieden werden:

  • Der Schlüssel K1 geht verloren, daher wird sein Speicher nie wiederhergestellt, bis der Prozess beendet wird. Da die Anzahl der Schlüssel begrenzt ist, sind möglicherweise nicht genügend Schlüssel vorhanden.
  • Wenn Thread A mit der Variablen theKey vor Aufruf 2 threadspezifische Daten speichert, werden die Daten an den Schlüssel K1gebunden. Nach Aufruf 2 enthält die Variable theKey den Wert K2; Wenn Thread A dann versucht, seine threadspezifischen Daten abzurufen, wird immer NULLabgerufen.

Es gibt folgende Möglichkeiten sicherzustellen, dass Schlüssel eindeutig erstellt werden:

  • Verwendung der einmaligen Initialisierungsfunktion.
  • Erstellen Sie den Schlüssel vor den Threads, die ihn verwenden. Dies ist häufig möglich, wenn beispielsweise ein Thread-Pool mit threadspezifischen Daten verwendet wird, um ähnliche Operationen auszuführen. Dieser Thread-Pool wird normalerweise von einem Thread erstellt, dem Anfangsthread (oder einem anderen "Treiberthread").

Es liegt in der Verantwortung des Programmierers, die Eindeutigkeit der Schlüsselerstellung sicherzustellen. Die Threadbibliothek bietet keine Möglichkeit zu prüfen, ob ein Schlüssel mehrmals erstellt wurde.

Destruktorroutine

Eine Destruktorroutine kann jedem threadspezifischen Datenschlüssel zugeordnet werden. Wenn ein Thread beendet wird und nichtNULL threadspezifische Daten für diesen Thread an einen beliebigen Schlüssel gebunden sind, wird die diesem Schlüssel zugeordnete Destruktorroutine aufgerufen. Auf diese Weise können dynamisch zugeordnete threadspezifische Daten automatisch freigegeben werden, wenn der Thread beendet wird. Die Destruktorroutine hat einen Parameter, den Wert der threadspezifischen Daten.

Beispielsweise kann ein threadspezifischer Datenschlüssel für dynamisch zugeordnete Puffer verwendet werden. Eine Destruktorroutine sollte bereitgestellt werden, um sicherzustellen, dass die Subroutine free beim Beenden des Threads wie folgt verwendet werden kann, wenn der Thread den Puffer beendet:
pthread_key_create(&key, free);
Komplexere Destruktoren können verwendet werden. Wenn ein Multithread-Befehl grep , der einen Thread pro Datei zum Durchsuchen verwendet, threadspezifische Daten zum Speichern einer Struktur enthält, die einen Arbeitspuffer und den Dateideskriptor des Threads enthält, kann die Destruktorroutine wie folgt aussehen:
typedef struct {
        FILE *stream;
        char *buffer;
} data_t;
...

void destructor(void *data)
{
        fclose(((data_t *)data)->stream);
        free(((data_t *)data)->buffer);
        free(data);
        *data = NULL;
}

Destruktoraufrufe können bis zu vier Mal wiederholt werden.

Schlüsselvernichtung

Ein threadspezifischer Datenschlüssel kann durch Aufrufen der Subroutine pthread_key_delete gelöscht werden. Die Subroutine pthread_key_delete ruft die Destruktorroutine für jeden Thread mit Daten nicht auf. Nachdem ein Datenschlüssel gelöscht wurde, kann er von einem anderen Aufruf der Subroutine pthread_key_create wiederverwendet werden. Daher ist die Subroutine pthread_key_delete besonders nützlich, wenn viele Datenschlüssel verwendet werden. Im folgenden Codefragment würde die Schleife beispielsweise nie enden:
/* bad example - do not write such code! */
pthread_key_t key;
 
while (pthread_key_create(&key, NULL))
        pthread_key_delete(key);

Threadspezifische Daten verwenden

Auf threadspezifische Daten wird mit den Subroutinen pthread_getspecific und pthread_setspecific zugegriffen. Die Subroutine pthread_getspecific liest den an den angegebenen Schlüssel gebundenen Wert und ist für den aufrufenden Thread spezifisch. Die Subroutine pthread_setspecific legt den Wert fest.

Aufeinanderfolgende Werte festlegen

Der an einen bestimmten Schlüssel gebundene Wert muss ein Zeiger sein, der auf jede Art von Daten verweisen kann. Threadspezifische Daten werden normalerweise für dynamisch zugeordneten Speicher verwendet, wie im folgenden Codefragment:
private_data = malloc(...);
pthread_setspecific(key, private_data);
Beim Festlegen eines Werts geht der vorherige Wert verloren. Im folgenden Codefragment geht beispielsweise der Wert des Zeigers old verloren und der Speicher, auf den er verweist, ist möglicherweise nicht wiederherstellbar:
pthread_setspecific(key, old);
...
pthread_setspecific(key, new);
Es liegt in der Verantwortung des Programmierers, den alten threadspezifischen Datenwert abzurufen, um Speicher freizugeben, bevor der neue Wert festgelegt wird. Beispielsweise kann eine swap_specific -Routine wie folgt implementiert werden:
int swap_specific(pthread_key_t key, void **old_pt, void *new)
{
        *old_pt = pthread_getspecific(key);
        if (*old_pt == NULL)
                return -1;
        else
                return pthread_setspecific(key, new);
}

Eine solche Routine ist in der Threadbibliothek nicht vorhanden, da es nicht immer erforderlich ist, den vorherigen Wert threadspezifischer Daten abzurufen. Ein solcher Fall tritt beispielsweise auf, wenn threadspezifische Daten Zeiger auf bestimmte Positionen in einem vom Anfangsthread zugeordneten Speicherpool sind.

Destruktorroutinen verwenden

Bei Verwendung dynamisch zugeordneter threadspezifischer Daten muss der Programmierer eine Destruktorroutine bereitstellen, wenn er die Subroutine pthread_key_create aufruft. Der Programmierer muss außerdem sicherstellen, dass bei der Freigabe des für threadspezifische Daten zugeordneten Speichers der Zeiger auf NULLgesetzt wird. Andernfalls kann die Destruktorroutine mit einem ungültigen Parameter aufgerufen werden. Beispiel:
pthread_key_create(&key, free);
...

...
private_data = malloc(...);
pthread_setspecific(key, private_data);
...

/* bad example! */
...
pthread_getspecific(key, &data);
free(data);
...

Wenn der Thread beendet wird, wird die Destruktorroutine für ihre threadspezifischen Daten aufgerufen. Da der Wert ein Zeiger auf bereits freigegebenen Speicher ist, kann ein Fehler auftreten. Um dies zu korrigieren, sollte das folgende Codefragment ersetzt werden:

/* better example! */
...
pthread_getspecific(key, &data);
free(data);
pthread_setspecific(key, NULL);
...

Wenn der Thread beendet wird, wird die Destruktorroutine nicht aufgerufen, da keine threadspezifischen Daten vorhanden sind.

Nicht-Zeigerwerte verwenden

Obwohl es möglich ist, Werte zu speichern, die keine Zeiger sind, wird dies aus den folgenden Gründen nicht empfohlen:

  • Die Umsetzung eines Zeigers in einen Skalartyp ist möglicherweise nicht übertragbar.
  • Der Zeigerwert NULL ist implementierungsabhängig. Mehrere Systeme weisen dem Zeiger NULL einen Wert ungleich null zu.

Wenn Sie sicher sind, dass Ihr Programm nie auf ein anderes System portiert wird, können Sie ganzzahlige Werte für threadspezifische Daten verwenden.