Qual é o conceito e o bloqueio do Reentrante em geral?

91

Eu sempre fico confuso. Alguém explicaria o que Reentrant significa em diferentes contextos? E por que você iria querer usar reentrante vs. não reentrante?

Diga primitivas de bloqueio pthread (posix), elas são reentrantes ou não? Que armadilhas devem ser evitadas ao usá-los?

O mutex é reentrante?

Vehomzzz
fonte

Respostas:

157

Bloqueio de reentrada

Um bloqueio reentrante é aquele em que um processo pode reivindicar o bloqueio várias vezes sem bloquear a si mesmo. É útil em situações em que não é fácil controlar se você já travou uma fechadura. Se um bloqueio não for reentrante, você pode agarrá-lo e bloqueá-lo quando for agarrá-lo novamente, bloqueando efetivamente seu próprio processo.

Em geral, a reentrância é uma propriedade do código em que não há estado mutável central que possa ser corrompido se o código for chamado durante a execução. Essa chamada pode ser feita por outro encadeamento ou recursivamente por um caminho de execução originado no próprio código.

Se o código depende de um estado compartilhado que pode ser atualizado no meio de sua execução, ele não é reentrante, pelo menos não se essa atualização puder interrompê-lo.

Um caso de uso para bloqueio reentrante

Um exemplo (um tanto genérico e artificial) de um aplicativo para um bloqueio reentrante pode ser:

  • Você tem alguns cálculos envolvendo um algoritmo que percorre um gráfico (talvez com ciclos). Uma passagem pode visitar o mesmo nó mais de uma vez devido aos ciclos ou devido a vários caminhos para o mesmo nó.

  • A estrutura de dados está sujeita a acesso simultâneo e pode ser atualizada por algum motivo, talvez por outro encadeamento. Você precisa ser capaz de bloquear nós individuais para lidar com a possível corrupção de dados devido a condições de corrida. Por algum motivo (talvez desempenho), você não deseja bloquear globalmente toda a estrutura de dados.

  • Seu cálculo não pode reter informações completas sobre quais nós você visitou, ou você está usando uma estrutura de dados que não permite que as perguntas do tipo 'já estive aqui antes' sejam respondidas rapidamente.

    Um exemplo dessa situação seria uma implementação simples do algoritmo de Dijkstra com uma fila de prioridade implementada como um heap binário ou uma pesquisa em largura usando uma lista vinculada simples como uma fila. Nesses casos, escanear a fila para inserções existentes é O (N) e você pode não querer fazer isso em todas as iterações.

Nessa situação, manter o controle de quais bloqueios você já adquiriu é caro. Supondo que você deseja fazer o bloqueio no nível do nó, um mecanismo de bloqueio reentrante alivia a necessidade de dizer se você já visitou um nó antes. Você pode apenas bloquear cegamente o nó, talvez desbloqueá-lo depois de retirá-lo da fila.

Mutexes reentrantes

Um mutex simples não é reentrante, pois apenas um thread pode estar na seção crítica em um determinado momento. Se você pegar o mutex e tentar pegá-lo novamente, um mutex simples não tem informações suficientes para dizer quem o estava segurando anteriormente. Para fazer isso recursivamente, você precisa de um mecanismo em que cada thread tenha um token para que você possa dizer quem pegou o mutex. Isso torna o mecanismo mutex um pouco mais caro, então você pode não querer fazer isso em todas as situações.

IIRC, a API de threads POSIX oferece a opção de mutexes re-entrantes e não re-entrantes.

ConcernedOfTunbridgeWells
fonte
2
Embora tais situações devam geralmente ser evitadas de qualquer maneira, pois também torna difícil evitar deadlock, etc. Encadear já é bastante difícil, sem dúvida se você já tem um bloqueio.
Jon Skeet de
+1, considere também o caso em que a fechadura NÃO é reentrante, você pode bloquear em si mesmo se não tiver cuidado. Além disso, em C, você não tem os mesmos mecanismos que outras linguagens têm para garantir que o bloqueio seja liberado quantas vezes for adquirido. Isso pode levar a grandes problemas.
user7116
1
foi exatamente o que aconteceu comigo ontem: não levei em consideração a questão da reentrada e acabei depurando um impasse por 5 horas ...
vehomzzz
@Jon Skeet - Acho que provavelmente há situações (veja meu exemplo um tanto artificial acima) em que controlar os bloqueios é impraticável devido ao desempenho ou outras considerações.
ConcernedOfTunbridgeWells
21

Um bloqueio reentrante permite escrever um método Mque coloca um bloqueio no recurso Ae, em seguida, chama Mrecursivamente ou a partir do código que já contém um bloqueio A.

Com um bloqueio não reentrante, você precisaria de 2 versões do M , uma que bloqueia e outra que não, e lógica adicional para chamar a correta.

Henk Holterman
fonte
Isso significa que, se eu tiver chamadas recursivas adquirindo o mesmo objeto de bloqueio mais de uma vez - digamos, xvezes por um determinado segmento, não posso intercalar a execução sem liberar todos os bloqueios adquiridos recursivamente (mesmo bloqueio, mas por xnúmero de vezes)? Se for verdadeiro, isso basicamente tornará essa implementação sequencial. Estou esquecendo de algo?
DevdattaK de
Isso não deveria ser um verdadeiro problema mundial. É mais sobre bloqueio granular e que um Thread não bloqueia a si mesmo.
Henk Holterman
15

O bloqueio reentrante é muito bem descrito neste tutorial .

O exemplo no tutorial é muito menos planejado do que na resposta sobre como percorrer um gráfico. Um bloqueio reentrante é útil em casos muito simples.

Ratna Beresford
fonte
3

O quê e por quê do mutex recursivo não deve ser algo tão complicado descrito na resposta aceita.

Eu gostaria de escrever meu entendimento depois de vasculhar a rede.


Primeiro, você deve perceber que, ao falar sobre mutex , os conceitos de multithread também estão definitivamente envolvidos. (mutex é usado para sincronização. Eu não preciso de mutex se eu tiver apenas 1 thread em meu programa)


Em segundo lugar, você deve saber a diferença entre um mutex normal e um mutex recursivo .

Citado de APUE :

(Um mutex recursivo é a) Um tipo de mutex que permite que o mesmo segmento o bloqueie várias vezes sem primeiro desbloqueá-lo.

A principal diferença é que dentro do mesmo encadeamento , travar novamente um bloqueio recursivo não leva a um deadlock, nem bloqueia o encadeamento.

Isso significa que o bloqueio recusivo nunca causa um impasse?
Não, ele ainda pode causar um deadlock como um mutex normal se você bloqueá-lo em um thread sem desbloqueá-lo e tentar bloqueá-lo em outros threads.

Vamos ver alguns códigos como prova.

  1. mutex normal com deadlock
#include <pthread.h>
#include <stdio.h>

pthread_mutex_t lock;


void * func1(void *arg){
    printf("thread1\n");
    pthread_mutex_lock(&lock);
    printf("thread1 hey hey\n");

}


void * func2(void *arg){
    printf("thread2\n");
    pthread_mutex_lock(&lock);
    printf("thread2 hey hey\n");
}

int main(){
    pthread_mutexattr_t lock_attr;
    int error;
//    error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
    error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_DEFAULT);
    if(error){
        perror(NULL);
    }

    pthread_mutex_init(&lock, &lock_attr);

    pthread_t t1, t2;

    pthread_create(&t1, NULL, func1, NULL);
    pthread_create(&t2, NULL, func2, NULL);

    pthread_join(t2, NULL);

}

resultado:

thread1
thread1 hey hey
thread2

exemplo de deadlock comum, sem problema.

  1. mutex recursivo com deadlock

Apenas descomente esta linha
error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
e comente a outra.

resultado:

thread1
thread1 hey hey
thread2

Sim, mutex recursivo também pode causar conflito.

  1. mutex normal, relock no mesmo segmento
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

pthread_mutex_t lock;


void func3(){
    printf("func3\n");
    pthread_mutex_lock(&lock);
    printf("func3 hey hey\n");
}

void * func1(void *arg){
    printf("thread1\n");
    pthread_mutex_lock(&lock);
    func3();
    printf("thread1 hey hey\n");

}


void * func2(void *arg){
    printf("thread2\n");
    pthread_mutex_lock(&lock);
    printf("thread2 hey hey\n");
}

int main(){
    pthread_mutexattr_t lock_attr;
    int error;
//    error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
    error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_DEFAULT);
    if(error){
        perror(NULL);
    }

    pthread_mutex_init(&lock, &lock_attr);

    pthread_t t1, t2;

    pthread_create(&t1, NULL, func1, NULL);
    sleep(2); 
    pthread_create(&t2, NULL, func2, NULL);

    pthread_join(t2, NULL);

}

resultado:

thread1
func3
thread2

Impasse em thread t1, em func3.
(Eu costumo sleep(2)tornar mais fácil ver que o impasse é causado primeiramente pelo relocking func3)

  1. mutex recursivo, relock no mesmo segmento

Novamente, descomente a linha mutex recursiva e comente a outra linha.

resultado:

thread1
func3
func3 hey hey
thread1 hey hey
thread2

Impasse em thread t2, em func2. Vejo? func3termina e sai, o relocking não bloqueia o thread ou leva a um deadlock.


Então, última pergunta, por que precisamos disso?

Para função recursiva (chamada em programas multi-threaded e você deseja proteger alguns recursos / dados).

Por exemplo, você tem um programa multi thread e chama uma função recursiva na thread A. Você tem alguns dados que deseja proteger nessa função recursiva, então usa o mecanismo mutex. A execução dessa função é sequencial no thread A, então você definitivamente travaria novamente o mutex em recursão. O uso de mutex normal causa bloqueios. E mutex resursivo é inventado para resolver isso.

Veja um exemplo da resposta aceita Quando usar mutex recursivo? .

A Wikipedia explica o mutex recursivo muito bem. Definitivamente vale a pena ler. Wikipedia: Reentrant_mutex

Rick
fonte