Wiedereintrittsfähigen und threadsicheren Code schreiben

In Einzelthreadprozessen ist nur ein Steuerungsfluss vorhanden. Der von diesen Prozessen ausgeführte Code muss daher nicht simultan oder threadsicher sein. In Multithread-Programmen können mehrere Steuerungsabläufe gleichzeitig auf dieselben Funktionen und Ressourcen zugreifen.

Um die Ressourcenintegrität zu schützen, muss der für Multithread-Programme geschriebene Code wiedereintrittsfähig und threadsicher sein.

Wiedereinstieg und Threadsicherheit hängen beide von der Art und Weise ab, wie Funktionen mit Ressourcen umgehen. Wiedereinstieg und Threadsicherheit sind separate Konzepte: Eine Funktion kann entweder wiedereintrittsfähig, threadsicher, beides oder keines von beiden sein.

Dieser Abschnitt enthält Informationen zum Schreiben von wiedereintrittsfähigen und threadsicheren Programmen. Das Thema des Schreibens von threadeffizienten Programmen wird nicht behandelt. Threadeffiziente Programme sind effizient parallelisierte Programme. Sie müssen die Threadeffizienz beim Entwurf des Programms berücksichtigen. Vorhandene Einzelthreadprogramme können threadeffizient gemacht werden, dies erfordert jedoch eine vollständige Neugestaltung und Neuerstellung.

Wiedereintritt

Eine wiedereintrittsfähige Funktion enthält keine statischen Daten über aufeinanderfolgende Aufrufe und gibt keinen Zeiger auf statische Daten zurück. Alle Daten werden vom Aufrufenden der Funktion bereitgestellt. Eine wiedereintrittsfähige Funktion darf keine nicht wiedereintrittsfähigen Funktionen aufrufen.

Eine nicht wiedereintrittsfähige Funktion kann häufig, aber nicht immer durch ihre externe Schnittstelle und ihre Nutzung identifiziert werden. Die Subroutine strtok ist beispielsweise nicht simultan verwendbar, da sie die Zeichenfolge enthält, die in Tokens aufgeteilt werden soll. Die Subroutine ctime ist ebenfalls nicht simultan verwendbar; sie gibt einen Zeiger auf statische Daten zurück, die von jedem Aufruf überschrieben werden.

Threadsicherheit

Eine threadsichere Funktion schützt gemeinsam genutzte Ressourcen vor gleichzeitigem Zugriff durch Sperren. Die Threadsicherheit betrifft nur die Implementierung einer Funktion und hat keine Auswirkungen auf ihre externe Schnittstelle.

In der Programmiersprache C werden lokale Variablen dynamisch im Stack zugeordnet. Daher ist jede Funktion, die keine statischen Daten oder andere gemeinsam genutzte Ressourcen verwendet, trivial threadsicher, wie im folgenden Beispiel gezeigt:
/* threadsafe function */
int diff(int x, int y)
{
        int delta;

        delta = y - x;
        if (delta < 0)
                delta = -delta;

        return delta;
}

Die Verwendung globaler Daten ist threadunsicher. Globale Daten sollten pro Thread verwaltet oder eingebunden werden, damit ihr Zugriff serialisiert werden kann. Ein Thread kann einen Fehlercode lesen, der einem Fehler entspricht, der von einem anderen Thread verursacht wurde. In AIX® hat jeder Thread seinen eigenen errno-Wert.

Funktion wiedereintrittsfähig machen

In den meisten Fällen müssen nicht wiedereintrittsfähige Funktionen durch Funktionen mit einer geänderten Schnittstelle ersetzt werden, um wiedereintrittsfähig zu sein. Nicht wiedereintrittsfähige Funktionen können nicht von mehreren Threads verwendet werden. Außerdem kann es unmöglich sein, eine nicht wiedereintrittsfähige Funktion threadsicher zu machen.

Daten zurückgeben

Viele nicht wiedereintrittsfähige Funktionen geben einen Zeiger auf statische Daten zurück. Dies kann wie folgt vermieden werden:
  • Dynamisch zugeordnete Daten zurückgeben. In diesem Fall liegt es in der Verantwortung des Anrufers, den Speicher freizugeben. Der Vorteil ist, dass die Schnittstelle nicht geändert werden muss. Die Abwärtskompatibilität ist jedoch nicht gewährleistet; vorhandene Einzelthreadprogramme, die die geänderten Funktionen ohne Änderungen verwenden, würden den Speicher nicht freigeben, was zu Speicherlecks führt.
  • Vom Aufrufenden bereitgestellter Speicher wird verwendet Diese Methode wird empfohlen, obwohl die Schnittstelle geändert werden muss.
Beispielsweise könnte eine Funktion strtoupper , die eine Zeichenfolge in Großbuchstaben konvertiert, wie im folgenden Codefragment implementiert werden:
/* non-reentrant function */
char *strtoupper(char *string)
{
        static char buffer[MAX_STRING_SIZE];
        int index;

        for (index = 0; string[index]; index++)
                buffer[index] = toupper(string[index]);
        buffer[index] = 0

        return buffer;
}
Diese Funktion ist nicht simultan verwendbar (noch threadsicher). Damit die Funktion wiedereintrittsfähig wird, indem dynamisch zugeordnete Daten zurückgegeben werden, würde die Funktion dem folgenden Codefragment ähneln:
/* reentrant function (a poor solution) */
char *strtoupper(char *string)
{
        char *buffer;
        int index;

        /* error-checking should be performed! */
        buffer = malloc(MAX_STRING_SIZE);

        for (index = 0; string[index]; index++)
                buffer[index] = toupper(string[index]);
        buffer[index] = 0

        return buffer;
}
Eine bessere Lösung besteht darin, die Schnittstelle zu ändern. Der Aufrufende muss den Speicher für Eingabe-und Ausgabezeichenfolgen bereitstellen, wie im folgenden Codefragment dargestellt:
/* reentrant function (a better solution) */
char *strtoupper_r(char *in_str, char *out_str)
{
        int index;

        for (index = 0; in_str[index]; index++)
        out_str[index] = toupper(in_str[index]);
        out_str[index] = 0

        return out_str;
}

Die nicht wiedereintrittsfähigen Standard-C-Bibliothekssubroutinen wurden wiedereintrittsfähig gemacht, indem Speicher verwendet wurde, der vom aufrufenden Programm bereitgestellt wurde.

Daten über aufeinanderfolgende Aufrufe hinweg beibehalten

Über aufeinanderfolgende Aufrufe sollten keine Daten aufbewahrt werden, da die Funktion von verschiedenen Threads nacheinander aufgerufen werden kann. Wenn eine Funktion einige Daten über aufeinanderfolgende Aufrufe verwalten muss, z. B. einen Arbeitspuffer oder einen Zeiger, sollte der Aufrufende diese Daten bereitstellen.

Sehen Sie sich das folgende Beispiel an. Eine Funktion gibt die aufeinanderfolgenden Kleinbuchstaben einer Zeichenfolge zurück. Die Zeichenfolge wird nur beim ersten Aufruf bereitgestellt, wie bei der Subroutine strtok . Die Funktion gibt 0 zurück, wenn sie das Ende der Zeichenfolge erreicht. Die Funktion könnte wie im folgenden Codefragment implementiert werden:
/* non-reentrant function */
char lowercase_c(char *string)
{
        static char *buffer;
        static int index;
        char c = 0;

        /* stores the string on first call */
        if (string != NULL) {
                buffer = string;
                index = 0;
        }

        /* searches a lowercase character */
        for (; c = buffer[index]; index++) {
                if (islower(c)) {
                        index++;
                        break;
                }
        }
        return c;
}

h

Diese Funktion ist nicht simultan verwendbar. Damit sie wiedereintrittsfähig wird, müssen die statischen Daten, die Variable index , vom Aufrufenden verwaltet werden. Die simultan verwendbare Version der Funktion kann wie im folgenden Codefragment implementiert werden:
/* reentrant function */
char reentrant_lowercase_c(char *string, int *p_index)
{
        char c = 0;

        /* no initialization - the caller should have done it */

        /* searches a lowercase character */
        for (; c = string[*p_index]; (*p_index)++) {
                if (islower(c)) {
                        (*p_index)++;
                        break;
                  }
        }
        return c;
}
Die Schnittstelle der Funktion hat sich und ihre Verwendung geändert. Der Aufrufende muss die Zeichenfolge bei jedem Aufruf bereitstellen und den Index vor dem ersten Aufruf mit 0 initialisieren, wie im folgenden Codefragment:
char *my_string;
char my_char;
int my_index;
...
my_index = 0;
while (my_char = reentrant_lowercase_c(my_string, &my_index)) {
        ...
}

Funktion threadsicher machen

In Multithread-Programmen müssen alle Funktionen, die von mehreren Threads aufgerufen werden, threadsicher sein. Es gibt jedoch eine Fehlerumgehung für die Verwendung von threadunsicheren Subroutinen in Multithread-Programmen. Nicht wiedereintrittsfähige Funktionen sind normalerweise threadunsicher, aber wenn sie wiedereintrittsfähig gemacht werden, sind sie oft auch threadsicher.

Gemeinsam genutzte Ressourcen sperren

Funktionen, die statische Daten oder andere gemeinsam genutzte Ressourcen wie Dateien oder Terminals verwenden, müssen den Zugriff auf diese Ressourcen durch Sperren serialisieren, um threadsicher zu sein. Die folgende Funktion ist beispielsweise threadunsicher:
/* thread-unsafe function */
int increment_counter()
{
        static int counter = 0;

        counter++;
        return counter;
}
Um threadsicher zu sein, muss die statische Variable counter wie im folgenden Beispiel durch eine statische Sperre geschützt werden:
/* pseudo-code threadsafe function */
int increment_counter();
{
        static int counter = 0;
        static lock_type counter_lock = LOCK_INITIALIZER;

        pthread_mutex_lock(counter_lock);
        counter++;
        pthread_mutex_unlock(counter_lock);
        return counter;
}

In einem Multithread-Anwendungsprogramm, das die Threadbibliothek verwendet, sollten Mutexe für die Serialisierung gemeinsam genutzter Ressourcen verwendet werden. Unabhängige Bibliotheken müssen möglicherweise außerhalb des Kontexts von Threads arbeiten und andere Arten von Sperren verwenden.

Problemumgehungen für threadunsichere Funktionen

Es ist möglich, eine Ausweichlösung zu verwenden, um threadunsichere Funktionen zu verwenden, die von mehreren Threads aufgerufen werden. Dies kann nützlich sein, insbesondere wenn Sie eine threadunsichere Bibliothek in einem Multithread-Programm zum Testen oder beim Warten auf eine threadsichere Version der Bibliothek verwenden. Die Problemumgehung führt zu einem gewissen Aufwand, da sie aus der Serialisierung der gesamten Funktion oder sogar einer Gruppe von Funktionen besteht. Folgende Problemumgehungen sind möglich:
  • Verwenden Sie eine globale Sperre für die Bibliothek und sperren Sie sie jedes Mal, wenn Sie die Bibliothek verwenden (Aufruf einer Bibliotheksroutine oder Verwendung einer globalen Bibliotheksvariablen). Diese Lösung kann Leistungsengpässe verursachen, da jeweils nur ein Thread auf jeden Teil der Bibliothek zugreifen kann. Die Lösung im folgenden Pseudocode ist nur akzeptabel, wenn selten auf die Bibliothek zugegriffen wird oder als erste, schnell implementierte Problemumgehung.
    /* this is pseudo code! */
    
    lock(library_lock);
    library_call();
    unlock(library_lock);
    
    lock(library_lock);
    x = library_var;
    unlock(library_lock);
  • Verwenden Sie eine Sperre für jede Bibliothekskomponente (Routine oder globale Variable) oder Komponentengruppe. Diese Lösung ist etwas komplizierter zu implementieren als das vorherige Beispiel, kann aber die Leistung verbessern. Da diese Problemumgehung nur in Anwendungsprogrammen und nicht in Bibliotheken verwendet werden sollte, können Mutexe zum Sperren der Bibliothek verwendet werden.
    /* this is pseudo-code! */
    
    lock(library_moduleA_lock);
    library_moduleA_call();
    unlock(library_moduleA_lock);
    
    lock(library_moduleB_lock);
    x = library_moduleB_var;
    unlock(library_moduleB_lock);

Wiedereintrittsfähige und threadsichere Bibliotheken

Wiedereintrittsfähige und threadsichere Bibliotheken sind in einer Vielzahl von parallelen (und asynchronen) Programmierumgebungen nützlich, nicht nur innerhalb von Threads. Es ist eine gute Programmierpraxis, immer wiedereintrittsfähige und threadsichere Funktionen zu verwenden und zu schreiben.

Bibliotheken verwenden

Mehrere Bibliotheken, die mit dem Basisbetriebssystem AIX geliefert werden, sind threadsicher. In der aktuellen Version von AIXsind die folgenden Bibliotheken threadsicher:
  • Standard-C-Bibliothek (libc.a)
  • Berkeley-Kompatibilitätsbibliothek (libbsd.a)

Einige der Standard-C-Subroutinen sind nicht wiedereintrittsfähig, z. B. die Subroutinen ctime und strtok . Die simultan verwendbare Version der Subroutine hat den Namen der ursprünglichen Subroutine mit dem Suffix _r (Unterstreichungszeichen gefolgt vom Buchstaben r).

Verwenden Sie beim Schreiben von Multithread-Programmen die simultan verwendbaren Versionen von Subroutinen anstelle der ursprünglichen Version. Beispiel: Das folgende Codefragment:
token[0] = strtok(string, separators);
i = 0;
do {
        i++;
        token[i] = strtok(NULL, separators);
} while (token[i] != NULL);
in einem Multithread-Programm durch das folgende Codefragment ersetzt werden:
char *pointer;
...
token[0] = strtok_r(string, separators, &pointer);
i = 0;
do {
        i++;
        token[i] = strtok_r(NULL, separators, &pointer);
} while (token[i] != NULL);

Threadunsichere Bibliotheken können nur von einem Thread in einem Programm verwendet werden. Stellen Sie die Eindeutigkeit des Threads sicher, der die Bibliothek verwendet. Andernfalls weist das Programm ein nicht erwartetes Verhalten auf oder kann sogar gestoppt werden.

Bibliotheken konvertieren

Beachten Sie Folgendes, wenn Sie eine vorhandene Bibliothek in eine wiedereintrittsfähige und threadsichere Bibliothek konvertieren. Diese Informationen gelten nur für Bibliotheken in der Programmiersprache C.
  • Geben Sie exportierte globale Variablen an. Diese Variablen werden normalerweise in einer Headerdatei mit dem Schlüsselwort export definiert. Exportierte globale Variablen sollten eingebunden werden. Die Variable sollte privat gemacht werden (definiert mit dem Schlüsselwort static im Quellcode der Bibliothek), und es sollten Zugriffssubroutinen (Lesen und Schreiben) erstellt werden.
  • Statische Variablen und andere gemeinsam genutzte Ressourcen identifizieren. Statische Variablen werden normalerweise mit dem Schlüsselwort static definiert. Sperren sollten allen gemeinsam genutzten Ressourcen zugeordnet sein. Die Granularität der Sperre, d. h. die Anzahl der Sperren, wirkt sich auf die Leistung der Bibliothek aus. Zum Initialisieren der Sperren kann die einmalige Initialisierungsfunktion verwendet werden.
  • Identifizieren Sie nicht wiedereintrittsfähige Funktionen und machen Sie sie wiedereintrittsfähig. Weitere Informationen finden Sie unter Funktion wiedereintrittsfähig machen.
  • Identifizieren Sie threadunsichere Funktionen und machen Sie sie threadsicher. Weitere Informationen finden Sie unter Making a Function threadsafe.