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.
/* 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
- 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.
/* 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;
}/* 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;
}/* 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.
/* 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
/* 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;
}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
/* thread-unsafe function */
int increment_counter()
{
static int counter = 0;
counter++;
return counter;
}/* 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
- 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
- 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).
token[0] = strtok(string, separators);
i = 0;
do {
i++;
token[i] = strtok(NULL, separators);
} while (token[i] != NULL);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
- 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.