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?
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.
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.
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.
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.
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.
Um bloqueio reentrante permite escrever um método
M
que coloca um bloqueio no recursoA
e, em seguida, chamaM
recursivamente ou a partir do código que já contém um bloqueioA
.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.fonte
x
vezes por um determinado segmento, não posso intercalar a execução sem liberar todos os bloqueios adquiridos recursivamente (mesmo bloqueio, mas porx
número de vezes)? Se for verdadeiro, isso basicamente tornará essa implementação sequencial. Estou esquecendo de algo?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.
fonte
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 :
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.
resultado:
exemplo de deadlock comum, sem problema.
Apenas descomente esta linha
error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
e comente a outra.
resultado:
Sim, mutex recursivo também pode causar conflito.
resultado:
Impasse em
thread t1
, emfunc3
.(Eu costumo
sleep(2)
tornar mais fácil ver que o impasse é causado primeiramente pelo relockingfunc3
)Novamente, descomente a linha mutex recursiva e comente a outra linha.
resultado:
Impasse em
thread t2
, emfunc2
. Vejo?func3
termina 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
fonte