Ecriture de code réentrant et autorisant les unités d'exécution multiples

Dans les processus à unité d'exécution unique, il n'existe qu'un seul flux de contrôle. Le code exécuté par ces processus n'a donc pas besoin d'être réentrant ou autorisant les unités d'exécution. Dans les programmes à unités d'exécution multiples, les mêmes fonctions et les mêmes ressources sont accessibles simultanément par plusieurs flux de contrôle.

Pour protéger l'intégrité des ressources, le code écrit pour les programmes à unités d'exécution multiples doit être réentrant et autorisant les unités d'exécution multiples.

La réentrée et la sécurité des unités d'exécution sont toutes deux liées à la façon dont les fonctions gèrent les ressources. La réentrée et la sécurité des unités d'exécution sont des concepts distincts: une fonction peut être réentrante, autorisant les unités d'exécution, les deux ou aucune des deux.

Cette section fournit des informations sur l'écriture de programmes réentrants et autorisant les unités d'exécution multiples. Il ne couvre pas la rubrique relative à l'écriture de programmes à unités d'exécution efficaces. Les programmes à unités d'exécution efficaces sont des programmes parallélisés de manière efficace. Vous devez tenir compte de l'efficacité des unités d'exécution lors de la conception du programme. Les programmes à unité d'exécution unique existants peuvent être rendus efficaces, mais cela nécessite qu'ils soient entièrement repensés et réécrits.

Réentrée

Une fonction réentrante ne contient pas de données statiques sur des appels successifs et ne renvoie pas de pointeur vers des données statiques. Toutes les données sont fournies par l'appelant de la fonction. Une fonction réentrante ne doit pas appeler de fonctions non réentrantes.

Une fonction non réentrante peut souvent, mais pas toujours, être identifiée par son interface externe et son utilisation. Par exemple, la sous-routine strtok n'est pas réentrante car elle contient la chaîne à diviser en jetons. La sous-routine ctime n'est pas non plus réentrante ; elle renvoie un pointeur vers des données statiques qui sont écrasées par chaque appel.

Sécurité des unités d'exécution

Une fonction autorisant les unités d'exécution multiples protège les ressources partagées des accès simultanés par des verrous. La sécurité des unités d'exécution ne concerne que l'implémentation d'une fonction et n'affecte pas son interface externe.

En langage C, les variables locales sont allouées dynamiquement sur la pile. Par conséquent, toute fonction qui n'utilise pas de données statiques ou d'autres ressources partagées est sécurisée par des unités d'exécution multiples, comme dans l'exemple suivant:
/* threadsafe function */
int diff(int x, int y)
{
        int delta;

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

        return delta;
}

L'utilisation des données globales n'est pas sûre pour les unités d'exécution. Les données globales doivent être conservées par unité d'exécution ou encapsulées, afin que leur accès puisse être sérialisé. Une unité d'exécution peut lire un code d'erreur correspondant à une erreur provoquée par une autre unité d'exécution. Sous AIX, chaque thread a sa propre valeur errno.

Rendre une fonction réentrante

Dans la plupart des cas, les fonctions non réentrantes doivent être remplacées par des fonctions avec une interface modifiée pour être réentrantes. Les fonctions non réentrantes ne peuvent pas être utilisées par plusieurs unités d'exécution. De plus, il peut être impossible de rendre une fonction non rentrante sans fil.

Renvoi de données

De nombreuses fonctions non réentrantes renvoient un pointeur vers des données statiques. Cela peut être évité des manières suivantes:
  • Renvoi de données allouées de manière dynamique. Dans ce cas, il incombera à l'appelant de libérer le stockage. L'avantage est que l'interface n'a pas besoin d'être modifiée. Cependant, la compatibilité avec les versions antérieures n'est pas assurée ; les programmes à unité d'exécution unique existants utilisant les fonctions modifiées sans modifications ne libéreraient pas le stockage, ce qui entraînerait des fuites de mémoire.
  • Utilisation du stockage fourni par l'appelant. Cette méthode est recommandée, bien que l'interface doive être modifiée.
Par exemple, une fonction strtoupper , qui convertit une chaîne en majuscules, peut être implémentée comme dans le fragment de code suivant:
/* 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;
}
Cette fonction n'est pas réentrante (ni autorisant les unités d'exécution multiples). Pour rendre la fonction réentrante en renvoyant des données allouées dynamiquement, la fonction serait similaire au fragment de code suivant:
/* 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;
}
Une meilleure solution consiste à modifier l'interface. L'appelant doit fournir le stockage pour les chaînes d'entrée et de sortie, comme dans le fragment de code suivant:
/* 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;
}

Les sous-routines de la bibliothèque C standard non réentrante ont été rendues réentrantes à l'aide de la mémoire fournie par l'appelant.

Conservation des données sur des appels successifs

Aucune donnée ne doit être conservée sur les appels successifs, car différentes unités d'exécution peuvent appeler successivement la fonction. Si une fonction doit conserver des données sur des appels successifs, tels qu'une mémoire tampon de travail ou un pointeur, l'appelant doit fournir ces données.

Prenons l'exemple suivant. Une fonction renvoie les caractères minuscules successifs d'une chaîne. La chaîne est fournie uniquement lors du premier appel, comme avec la sous-routine strtok . La fonction renvoie 0 lorsqu'elle atteint la fin de la chaîne. La fonction peut être implémentée comme dans le fragment de code suivant:
/* 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

Cette fonction n'est pas réentrante. Pour le rendre réentrant, les données statiques, la variable index , doivent être gérées par l'appelant. La version réentrante de la fonction peut être implémentée comme dans le fragment de code suivant:
/* 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;
}
L'interface de la fonction a changé, de même que son utilisation. L'appelant doit fournir la chaîne à chaque appel et doit initialiser l'index à 0 avant le premier appel, comme dans le fragment de code suivant:
char *my_string;
char my_char;
int my_index;
...
my_index = 0;
while (my_char = reentrant_lowercase_c(my_string, &my_index)) {
        ...
}

Rendre une fonction autorisant les unités d'exécution multiples

Dans les programmes à unités d'exécution multiples, toutes les fonctions appelées par plusieurs unités d'exécution doivent être autorisant les unités d'exécution multiples. Toutefois, il existe une solution palliative pour l'utilisation de sous-routines non sécurisées par des unités d'exécution dans des programmes à unités d'exécution multiples. Les fonctions non réentrantes ne sont généralement pas sûres pour les unités d'exécution, mais les rendre réentrantes les rend souvent également sûres pour les unités d'exécution.

Verrouillage des ressources partagées

Les fonctions qui utilisent des données statiques ou d'autres ressources partagées, telles que des fichiers ou des terminaux, doivent sérialiser l'accès à ces ressources par des verrous afin d'autoriser les unités d'exécution multiples. Par exemple, la fonction suivante est non sécurisée pour les unités d'exécution:
/* thread-unsafe function */
int increment_counter()
{
        static int counter = 0;

        counter++;
        return counter;
}
Pour que les unités d'exécution soient protégées, la variable statique counter doit être protégée par un verrou statique, comme dans l'exemple suivant:
/* 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;
}

Dans un programme d'application à unités d'exécution multiples utilisant la bibliothèque d'unités d'exécution, les mutex doivent être utilisés pour la sérialisation des ressources partagées. Les bibliothèques indépendantes peuvent avoir besoin de travailler en dehors du contexte des unités d'exécution et, par conséquent, utiliser d'autres types de verrous.

Solutions de contournement pour les fonctions non sécurisées par des unités d'exécution

Il est possible d'utiliser une solution palliative pour utiliser des fonctions d'unités d'exécution non sécurisées appelées par plusieurs unités d'exécution. Cela peut être utile, en particulier lors de l'utilisation d'une bibliothèque à unités d'exécution non sécurisée dans un programme à unités d'exécution multiples, à des fins de test ou en attendant qu'une version autorisant les unités d'exécution multiples de la bibliothèque soit disponible. La solution de contournement entraîne une surcharge, car elle consiste à sérialiser l'ensemble de la fonction ou même un groupe de fonctions. Les solutions de contournement possibles sont les suivantes:
  • Utilisez un verrou global pour la bibliothèque et verrouillez-le chaque fois que vous utilisez la bibliothèque (en appelant une routine de bibliothèque ou en utilisant une variable globale de bibliothèque). Cette solution peut créer des goulots d'étranglement des performances car une seule unité d'exécution peut accéder à une partie de la bibliothèque à la fois. La solution du pseudocode suivant n'est acceptable que si l'accès à la bibliothèque est rare, ou en tant que solution de contournement initiale rapidement implémentée.
    /* this is pseudo code! */
    
    lock(library_lock);
    library_call();
    unlock(library_lock);
    
    lock(library_lock);
    x = library_var;
    unlock(library_lock);
  • Utilisez un verrou pour chaque composant de bibliothèque (routine ou variable globale) ou groupe de composants. Cette solution est un peu plus compliquée à implémenter que l'exemple précédent, mais elle peut améliorer les performances. Etant donné que cette solution de contournement ne doit être utilisée que dans les programmes d'application et non dans les bibliothèques, des exclusions mutuelles peuvent être utilisées pour verrouiller la bibliothèque.
    /* 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);

Bibliothèques réentrantes et autorisant les unités d'exécution multiples

Les bibliothèques réentrantes et les bibliothèques autorisant les unités d'exécution multiples sont utiles dans un large éventail d'environnements de programmation parallèles (et asynchrones), et pas seulement dans les unités d'exécution. Il est recommandé de toujours utiliser et d'écrire des fonctions réentrantes et autorisant les unités d'exécution multiples.

Utilisation de bibliothèques

Plusieurs bibliothèques fournies avec le système d'exploitation de base AIX sont autorisant les unités d'exécution multiples. Dans la version actuelle d' AIX, les bibliothèques suivantes autorisent les unités d'exécution multiples:
  • Bibliothèque C standard (libc.a)
  • Bibliothèque de compatibilité Berkeley (libbsd.a)

Certaines des sous-routines C standard sont non réentrantes, telles que les sous-routines ctime et strtok . La version réentrante des sous-routines a le nom de la sous-routine d'origine avec le suffixe _r (trait de soulignement suivi de la lettre r).

Lors de l'écriture de programmes à unités d'exécution multiples, utilisez les versions réentrantes des sous-programmes à la place de la version d'origine. Par exemple, le fragment de code suivant:
token[0] = strtok(string, separators);
i = 0;
do {
        i++;
        token[i] = strtok(NULL, separators);
} while (token[i] != NULL);
doivent être remplacés dans un programme à unités d'exécution multiples par le fragment de code suivant:
char *pointer;
...
token[0] = strtok_r(string, separators, &pointer);
i = 0;
do {
        i++;
        token[i] = strtok_r(NULL, separators, &pointer);
} while (token[i] != NULL);

Les bibliothèques non sécurisées peuvent être utilisées par une seule unité d'exécution dans un programme. Vérifiez l'unicité de l'unité d'exécution utilisant la bibliothèque ; sinon, le programme aura un comportement inattendu, ou peut même s'arrêter.

Conversion de bibliothèques

Tenez compte des points suivants lors de la conversion d'une bibliothèque existante en bibliothèque réentrante et autorisant les unités d'exécution multiples. Ces informations s'appliquent uniquement aux bibliothèques de langues C.
  • Identifiez les variables globales exportées. Ces variables sont généralement définies dans un fichier d'en-tête avec le mot clé export . Les variables globales exportées doivent être encapsulées. La variable doit être rendue privée (définie avec le mot clé static dans le code source de la bibliothèque) et des sous-routines d'accès (lecture et écriture) doivent être créées.
  • Identifiez les variables statiques et les autres ressources partagées. Les variables statiques sont généralement définies avec le mot clé static . Les verrous doivent être associés à n'importe quelle ressource partagée. La granularité du verrouillage, choisissant ainsi le nombre de verrous, a un impact sur les performances de la bandothèque. Pour initialiser les verrous, la fonction d'initialisation unique peut être utilisée.
  • Identifier les fonctions non réentrantes et les rendre réentrantes. Pour plus d'informations, voir Rendre une fonction réentrante.
  • Identifiez les fonctions non sécurisées par des unités d'exécution et rendez ces fonctions sécurisées par des unités d'exécution. Pour plus d'informations, voir Création d'une fonction autorisant les unités d'exécution multiples.