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.

En lenguaje C, las variables locales se asignan dinámicamente en la pila. Por lo tanto, cualquier función que no utilice datos estáticos u otros recursos compartidos es trivialmente segura en hebras, como en el ejemplo siguiente:
/* 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

Muchas funciones no reentrantes devuelven un puntero a datos estáticos. Esto se puede evitar de las siguientes maneras:
  • 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.
Por ejemplo, una función strtoupper , que convierte una serie a mayúsculas, podría implementarse como en el fragmento de código siguiente:
/* 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;
}
Esta función no es reentrante (ni de hebras seguras). Para hacer que la función vuelva a entrar devolviendo datos asignados dinámicamente, la función sería similar al fragmento de código siguiente:
/* 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;
}
Una mejor solución consiste en modificar la interfaz. El interlocutor debe proporcionar el almacenamiento para las series de entrada y salida, como en el fragmento de código siguiente:
/* 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.

Veamos un ejemplo: Una función devuelve los caracteres en minúsculas sucesivos de una serie. La serie sólo se proporciona en la primera llamada, como con la subrutina strtok . La función devuelve 0 cuando alcanza el final de la serie. La función podría implementarse como en el siguiente fragmento de código:
/* 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

Esta función no es reentrante. Para que sea reentrante, el llamante debe mantener los datos estáticos, la variable index . La versión reentrante de la función podría implementarse como en el siguiente fragmento de código:
/* 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;
}
La interfaz de la función ha cambiado y también lo ha hecho su uso. El llamante debe proporcionar la serie en cada llamada y debe inicializar el índice en 0 antes de la primera llamada, como en el fragmento de código siguiente:
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

Las funciones que utilizan datos estáticos o cualquier otro recurso compartido, como archivos o terminales, deben serializar el acceso a estos recursos mediante bloqueos para que sean de hebra segura. Por ejemplo, la siguiente función no es segura para hebras:
/* thread-unsafe function */
int increment_counter()
{
        static int counter = 0;

        counter++;
        return counter;
}
Para ser de hebra segura, la variable estática counter debe estar protegida por un bloqueo estático, como en el ejemplo siguiente:
/* 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

Es posible utilizar una solución temporal para utilizar funciones no seguras de hebra llamadas por varias hebras. Esto puede ser útil, especialmente cuando se utiliza una biblioteca no segura de hebras en un programa multihebra, para realizar pruebas o mientras se espera a que esté disponible una versión segura de hebras de la biblioteca. La solución temporal lleva a cierta sobrecarga, porque consiste en serializar toda la función o incluso un grupo de funciones. Los métodos alternativos posibles son los siguientes:
  • 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

Varias bibliotecas que se suministran con el sistema operativo base AIX son seguras en ejecución multihebra. En la versión actual de AIX, las bibliotecas siguientes son seguras en ejecución multihebra:
  • 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).

Al escribir programas multihebra, utilice las versiones reentrantes de las subrutinas en lugar de la versión original. Por ejemplo, el fragmento de código siguiente:
token[0] = strtok(string, separators);
i = 0;
do {
        i++;
        token[i] = strtok(NULL, separators);
} while (token[i] != NULL);
debe sustituirse en un programa multihebra por el siguiente fragmento de código:
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

Tenga en cuenta lo siguiente al convertir una biblioteca existente en una biblioteca reentrante y de hebras seguras. Esta información sólo se aplica a las bibliotecas de lenguaje C.
  • 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.