Código reentrante e código de rossão

Em processos de encadeados únicos, apenas um fluxo de controle existe. O código executado por esses processos, portanto, não precisa ser reentrante ou threadsafe. Em programas multithreaded, as mesmas funções e os mesmos recursos podem ser acessados simultaneamente por vários fluxos de controle.

Para proteger a integridade do recurso, o código escrito para programas multiencadeados deve ser reentrante e rossseguro.

A reentrada e a segurança do fio estão ambas relacionadas com a forma que as funções manejam recursos. Reentrada e segurança de encadeamento são conceitos separados: uma função pode ser reentrante, threadsafe, ambas, ou nem tampouco.

Esta seção fornece informações sobre a escrita de programas reentrantes e rosseguros. Ele não cobre o tópico de escrever programas eficientes de encadeamento. Programas eficientes de encadeamento são programas paralelizados eficientemente. Você deve considerar a eficiência de encadeamento durante o design do programa. Os programas de threaded existentes podem ser feitos de forma eficiente, mas isso requer que eles sejam completamente redesenhados e reescritos.

Reentrada

Uma função reentrante não mantém dados estáticos sobre chamadas sucessivas, nem devolvem um ponteiro aos dados estáticos. Todos os dados são fornecidos pelo ouvinte da função. Uma função reentrante não deve chamar funções não reentrantes.

Uma função não reentrante pode muitas vezes, mas nem sempre, ser identificada por sua interface externa e seu uso. Por exemplo, o subroutine strtok não é reentrante, pois ele mantém a string a ser quebrada em tokens. O subroutine ctime também não é reentrante; ele retorna um ponteiro para dados estáticos que são sobrescritos por cada chamada.

Segurança do fio

Uma função threadsafe protege recursos compartilhados de acessos simultâneos por bloqueios. A segurança do fio diz respeito apenas à implementação de uma função e não afeta a sua interface externa.

Em linguagem C, as variáveis locais são alocadas dinamicamente na pilha. Portanto, qualquer função que não utilize dados estáticos ou outros recursos compartilhados é trivialmente threadsafe, como no exemplo a seguir:
/* threadsafe function */
int diff(int x, int y)
{
        int delta;

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

        return delta;
}

O uso de dados globais é thread-insafe. Os dados globais devem ser mantidos por thread ou encapsulados, de modo que seu acesso possa ser serializado. Uma thread pode ler um código de erro correspondente a um erro causado por outro encadeamento. No AIX®, cada thread tem seu próprio valor errno.

Fazendo uma função reentrante

Na maioria dos casos, as funções não reentrantes devem ser substituídas por funções com uma interface modificada para ser reentrante. Funções não reentrantes não podem ser usadas por várias threads. Além disso, pode ser impossível fazer uma função não reentrante threadsafe.

Retornando dados

Muitas funções não reentrantes retornam um ponteiro para os dados estáticos. Isso pode ser evitado das seguintes formas:
  • Retornando dados alocados dinamicamente. Neste caso, será a responsabilidade do ouvinte liberar o armazenamento. O benefício é que a interface não precisa ser modificada. No entanto, a compatibilidade com backward não é assegurada; os programas de threaded existentes usando as funções modificadas sem alterações não liberariam o armazenamento, levando a vazamentos de memória.
  • Usando armazenamento fornecido pelo caller. Este método é recomendado, embora a interface deve ser modificada.
Por exemplo, uma função strtoupper , convertendo uma string em maiúsculas, poderia ser implementada como no fragmento de código a seguir:
/* 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 função não é reentrante (nem treadsafe). Para tornar a função reentrante retornando dados alocados dinamicamente, a função seria semelhante ao fragmento de código a seguir:
/* 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;
}
Uma solução melhor consiste em modificar a interface. O ouvinte deve fornecer o armazenamento para as strings de entrada e saída, como no fragmento de código a seguir:
/* 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;
}

As subroutines de biblioteca padrão C não reentrantes foram tornadas reentrantes usando armazenamento fornecido pelo caller.

Manter os dados sobre as chamadas sucessivas

Nenhum dado deve ser mantido sobre as chamadas sucessivas, pois diferentes threads podem sucessivamente chamar a função. Se uma função deve manter alguns dados sobre chamadas sucessivas, como um buffer de trabalho ou um ponteiro, o ouvinte deverá fornecer esses dados.

Considere o exemplo a seguir. Uma função retorna os sucessivos caracteres minúsos de uma string. A string é fornecida apenas na primeira chamada, como com a subroutine strtok . A função retorna 0 quando atinge o final da string. A função poderia ser implementada como no fragmento de código a seguir:
/* 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 função não é reentrante. Para torná-lo reentrante, os dados estáticos, a variável index , devem ser mantidos pelo responsável pelo chamamento. A versão reentrante da função poderia ser implementada como no fragmento de código a seguir:
/* 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;
}
A interface da função mudou e assim fez o seu uso. O ouvinte deve fornecer a string em cada chamada e deve inicializar o índice até 0 minutos antes da primeira chamada, como no fragmento de código a seguir:
char *my_string;
char my_char;
int my_index;
...
my_index = 0;
while (my_char = reentrant_lowercase_c(my_string, &my_index)) {
        ...
}

Fazendo uma função threadsafe

Em programas multithreaded, todas as funções chamadas por múltiplas threads devem ser threadsafe. No entanto, uma solução alternativa existe para usar sub-rotinas sem segurança em programas multithreaded. Funções não reentrantes geralmente são thread-inseguras, mas fazê-las reentrantes muitas vezes torna-as a treadsafe, também.

Bloqueio de recursos compartilhados

Funções que utilizam dados estáticos ou quaisquer outros recursos compartilhados, como arquivos ou terminais, devem serializar o acesso a esses recursos por bloqueios, a fim de serem threadsafe. Por exemplo, a função a seguir é thread-insafe:
/* thread-unsafe function */
int increment_counter()
{
        static int counter = 0;

        counter++;
        return counter;
}
Para ser threadsafe, a variável estática counter deve ser protegida por um bloqueio estático, como no exemplo a seguir:
/* 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;
}

Em um programa de aplicativos multiencadeados usando a biblioteca threads, mutexes devem ser usados para serialização de recursos compartilhados. As bibliotecas independentes podem precisar trabalhar fora do contexto de threads e, assim, utilizar outros tipos de bloqueios.

Workarounds para funções inseguras de thread

É possível usar um workaround para usar funções inseguras de thread chamadas por várias threads. Isso pode ser útil, especialmente ao usar uma biblioteca insegura de thread em um programa multiencadeado, para testes ou enquanto espera uma versão threadsafe da biblioteca a ser disponível. A solução alternativa leva a algum overhead, pois consiste em serializar toda a função ou até mesmo um grupo de funções. A seguir são possíveis alternativas de trabalho:
  • Use um bloqueio global para a biblioteca, e bloqueie-a cada vez que você usar a biblioteca (chamando uma rotina de biblioteca ou usando uma variável global de biblioteca). Esta solução pode criar gargalos de desempenho porque apenas uma thread pode acessar qualquer parte da biblioteca em qualquer momento. A solução no seguinte pseudocódigo é aceitável apenas se a biblioteca raramente for acessada, ou como inicial, rapidamente implementada a solução alternativa.
    /* this is pseudo code! */
    
    lock(library_lock);
    library_call();
    unlock(library_lock);
    
    lock(library_lock);
    x = library_var;
    unlock(library_lock);
  • Use um bloqueio para cada componente da biblioteca (rotina ou variável global) ou grupo de componentes. Essa solução é um pouco mais complicada de implementar do que o exemplo anterior, mas pode melhorar o desempenho. Como esse workaround só deve ser usado em programas de aplicativos e não em bibliotecas, mutexes podem ser usados para bloqueio da 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 reentrantes e reajustes

As bibliotecas reentrantes e reajustes são úteis em uma ampla gama de ambientes de programação paralelos (e assíncronos), não apenas dentro de threads. É uma boa prática de programação para sempre utilizar e escrever funções reentrantes e de rossão.

Usando bibliotecas

Várias bibliotecas enviadas com o Sistema Operacional de Base AIX são threadsafe. Na versão atual do AIX, as bibliotecas a seguir são threadsafe:
  • Biblioteca C padrão (libc.a)
  • Biblioteca de compatibilidade Berkeley (libbsd.a)

Algumas das subroutines C padrão são não reentrantes, como as subroutines ctime e strtok . A versão reentrante das subroutines tem o nome da subroutina original com um sufixo _r (sublinhado seguido da letra r).

Ao escrever programas multithreaded, use as versões reentrantes de subroutines em vez da versão original. Por exemplo, o fragmento de código a seguir:
token[0] = strtok(string, separators);
i = 0;
do {
        i++;
        token[i] = strtok(NULL, separators);
} while (token[i] != NULL);
deve ser substituído em um programa multiencadeado pelo fragmento de código a seguir:
char *pointer;
...
token[0] = strtok_r(string, separators, &pointer);
i = 0;
do {
        i++;
        token[i] = strtok_r(NULL, separators, &pointer);
} while (token[i] != NULL);

Bibliotecas inseguras de encadeamento podem ser usadas por apenas uma thread em um programa. Garantir a exclusividade do encadeamento usando a biblioteca; caso contrário, o programa terá um comportamento inesperado, ou pode até parar.

Convertendo bibliotecas

Considere o seguinte ao converter uma biblioteca existente em uma biblioteca reentrante e encadeada. Essas informações se aplicam apenas às bibliotecas de idiomas C.
  • Identificar variáveis globais exportadas. Essas variáveis geralmente são definidas em um arquivo de cabeçalho com a palavra-chave export . As variáveis globais exportadas devem ser encapsuladas. A variável deve ser feita privada (definida com a palavra-chave static no código-fonte da biblioteca) e as subroutines de acesso (leitura e escrita) devem ser criadas.
  • Identificar variáveis estáticas e outros recursos compartilhados. As variáveis estáticas são geralmente definidas com a palavra-chave static . Os bloqueios devem ser associados a qualquer recurso compartilhado. A granularidade do travamento, escolhendo assim o número de bloqueios, impacta o desempenho da biblioteca. Para inicializar os bloqueios, pode ser utilizada a instalação de inicialização única.
  • Identificar funções não reentrantes e torná-las reentrantes. Para obter mais informações, consulte Making a Function Reentrant.
  • Identificar funções inseguras de thread e torná-las threadsafe. Para obter mais informações, consulte Making a Function threadsafe.