Escritura de código reentrante y de hebra segura
En procesos de una sola hebra, sólo existe un flujo de control. Por lo tanto, no es necesario que el código ejecutado por estos procesos sea reentrante o multihebra. En programas multihebra, varios flujos de control pueden acceder simultáneamente a las mismas funciones y a los mismos recursos.
Para proteger la integridad de los recursos, el código escrito para programas multihebra debe ser reentrante y multihebra.
La reentrada y la seguridad de hebras están relacionadas con la forma en que las funciones manejan los recursos. La seguridad de reentrada y de hebras son conceptos separados: una función puede ser reentrante, segura de hebras, ambas o ninguna.
Esta sección proporciona información sobre cómo escribir programas reentrantes y de hebras seguras. No cubre el tema de la escritura de programas de hebra eficiente. Los programas de hebra eficiente son programas paralelizados de forma eficiente. Debe tener en cuenta la eficiencia de hebras durante el diseño del programa. Los programas de una sola hebra existentes se pueden hacer eficientes en las hebras, pero esto requiere que se rediseñen y reescriban completamente.
Reentrada
Una función reentrante no contiene datos estáticos en llamadas sucesivas, ni devuelve un puntero a datos estáticos. Todos los datos los proporciona el interlocutor de la función. Una función reentrante no debe llamar a funciones no reentrantes.
Una función no reentrante a menudo, pero no siempre, puede ser identificada por su interfaz externa y su uso. Por ejemplo, la subrutina strtok no es reentrante, porque contiene la serie que se debe dividir en señales. La subrutina ctime tampoco es reentrante; devuelve un puntero a los datos estáticos que sobrescribe cada llamada.
Seguridad de hebras
Una función de hebra segura protege los recursos compartidos del acceso simultáneo mediante bloqueos. La seguridad de hebras sólo afecta a la implementación de una función y no afecta a su interfaz externa.
/* threadsafe function */
int diff(int x, int y)
{
int delta;
delta = y - x;
if (delta < 0)
delta = -delta;
return delta;
}El uso de datos globales no es seguro para hebras. Los datos globales se deben mantener por hebra o encapsulados, de modo que se pueda serializar su acceso. Una hebra puede leer un código de error correspondiente a un error causado por otra hebra. En AIX®, cada hilo tiene su propio valor errno.
Cómo hacer que una función vuelva a entrar
En la mayoría de los casos, las funciones no reentrantes deben sustituirse por funciones con una interfaz modificada para ser reentrantes. Las funciones no reentrantes no pueden ser utilizadas por varias hebras. Además, puede ser imposible hacer que una función no reentrante sea segura en ejecución multihebra.
Devolución de datos
- Devolviendo datos asignados dinámicamente. En este caso, será responsabilidad del llamante liberar el almacenamiento. La ventaja es que no es necesario modificar la interfaz. Sin embargo, la compatibilidad con versiones anteriores no está garantizada; los programas de una sola hebra existentes que utilizan las funciones modificadas sin cambios no liberarían el almacenamiento, lo que llevaría a fugas de memoria.
- Se utiliza el almacenamiento proporcionado por el llamante. Se recomienda este método, aunque se debe modificar la interfaz.
/* 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;
}Las subrutinas de la biblioteca C estándar no reentrante se han hecho reentrantes utilizando el almacenamiento proporcionado por el llamante.
Mantenimiento de datos en llamadas sucesivas
No se deben mantener datos sobre llamadas sucesivas, porque diferentes hebras pueden llamar sucesivamente a la función. Si una función debe mantener algunos datos en llamadas sucesivas, como un almacenamiento intermedio de trabajo o un puntero, el llamante debe proporcionar estos datos.
/* 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)) {
...
}Cómo hacer que una función sea segura en ejecución multihebra
En programas multihebra, todas las funciones llamadas por varias hebras deben ser de hebra segura. Sin embargo, existe una solución temporal para utilizar subrutinas no seguras de hebras en programas multihebra. Las funciones no reentrantes normalmente no son seguras en hebras, pero hacerlas reentrantes a menudo también las hacen seguras en hebras.
Bloqueo de recursos compartidos
/* 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;
}En un programa de aplicación multihebra que utiliza la biblioteca de hebras, se deben utilizar los mutexes para serializar los recursos compartidos. Las bibliotecas independientes pueden necesitar trabajar fuera del contexto de las hebras y, por lo tanto, utilizar otros tipos de bloqueos.
Métodos alternativos para funciones no seguras de hebras
- Utilice un bloqueo global para la biblioteca y bloquéelo cada vez que utilice la biblioteca (llamando a una rutina de biblioteca o utilizando una variable global de biblioteca). Esta solución puede crear cuellos de botella de rendimiento porque sólo una hebra puede acceder a cualquier parte de la biblioteca en un momento determinado. La solución en el pseudocódigo siguiente sólo es aceptable si se accede a la biblioteca con poca frecuencia, o como solución temporal inicial implementada rápidamente.
/* this is pseudo code! */ lock(library_lock); library_call(); unlock(library_lock); lock(library_lock); x = library_var; unlock(library_lock); - Utilice un bloqueo para cada componente de biblioteca (rutina o variable global) o grupo de componentes. Esta solución es algo más complicada de implementar que el ejemplo anterior, pero puede mejorar el rendimiento. Puesto que esta solución temporal sólo se debe utilizar en programas de aplicación y no en bibliotecas, se pueden utilizar exclusiones mutuas para bloquear la biblioteca.
/* 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);
Bibliotecas de reentrada y de hebras seguras
Las bibliotecas de reentrada y enhebramiento seguro son útiles en una amplia gama de entornos de programación paralelos (y asíncronos), no sólo dentro de las hebras. Es una buena práctica de programación utilizar y escribir siempre funciones reentrantes y de hebras seguras.
Utilización de bibliotecas
- Biblioteca C estándar (libc.a)
- Biblioteca de compatibilidad de Berkeley (libbsd.a)
Algunas de las subrutinas C estándar no son reentrantes, como las subrutinas ctime y strtok . La versión reentrante de las subrutinas tiene el nombre de la subrutina original con un sufijo _r (subrayado seguido de la letra 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);Sólo una hebra de un programa puede utilizar las bibliotecas que no son seguras. Asegúrese de la exclusividad de la hebra que utiliza la biblioteca; de lo contrario, el programa tendrá un comportamiento inesperado, o incluso puede detenerse.
Conversión de bibliotecas
- Identifique las variables globales exportadas. Estas variables se definen normalmente en un archivo de cabecera con la palabra clave export . Las variables globales exportadas deben estar encapsuladas. La variable se debe hacer privada (definida con la palabra clave static en el código fuente de la biblioteca) y se deben crear subrutinas de acceso (lectura y escritura).
- Identifique variables estáticas y otros recursos compartidos. Las variables estáticas se definen normalmente con la palabra clave static . Los bloqueos deben estar asociados a cualquier recurso compartido. La granularidad del bloqueo, eligiendo así el número de bloqueos, afecta al rendimiento de la biblioteca. Para inicializar los bloqueos, se puede utilizar el recurso de inicialización de una sola vez.
- Identificar las funciones no reentrantes y hacerlas reentrantes. Para obtener más información, consulte Cómo realizar una función reentrante.
- Identifique las funciones que no son seguras para hebras y haga que sean seguras para hebras. Para obtener más información, consulte Cómo hacer que una función sea segura en ejecución multihebra.