A maioria dos os tempos , a definição de reentrada é citado de Wikipedia :
Um programa ou rotina de computador é descrito como reentrante, se puder ser chamado com segurança novamente antes que sua chamada anterior seja concluída (isto é, pode ser executada com segurança simultaneamente). Para reentrar, um programa ou rotina de computador:
- Não deve conter dados estáticos (ou globais) não constantes.
- Não deve retornar o endereço para dados estáticos (ou globais) não constantes.
- Deve funcionar apenas nos dados fornecidos pelo responsável pela chamada.
- Não deve confiar em bloqueios para recursos singleton.
- Não deve modificar seu próprio código (a menos que seja executado em seu próprio armazenamento de encadeamento exclusivo)
- Não deve chamar programas ou rotinas de computador não reentrantes.
Como é definido com segurança ?
Se um programa pode ser executado com segurança simultaneamente , isso sempre significa que é reentrante?
Qual é exatamente o encadeamento comum entre os seis pontos mencionados que devo ter em mente ao verificar meu código quanto a recursos de reentrada?
Além disso,
- Todas as funções recursivas são reentrantes?
- Todas as funções de thread-safe são reentrantes?
- Todas as funções recursivas e com thread-safe são reentrantes?
Ao escrever esta pergunta, uma coisa vem à mente: os termos como reentrada e segurança de linha são absolutos, ou seja, eles têm definições concretas fixas? Pois, se não forem, essa questão não é muito significativa.
fonte
Respostas:
1. Como é definido com segurança ?
Semanticamente. Nesse caso, este não é um termo definido de forma rígida. Significa apenas "Você pode fazer isso, sem risco".
2. Se um programa pode ser executado com segurança simultaneamente, isso sempre significa que é reentrante?
Não.
Por exemplo, vamos ter uma função C ++ que aceita um bloqueio e um retorno de chamada como parâmetro:
Outra função pode precisar bloquear o mesmo mutex:
À primeira vista, tudo parece bem ... Mas espere:
Se o bloqueio no mutex não for recursivo, eis o que acontecerá, no thread principal:
main
vai ligarfoo
.foo
adquirirá o bloqueio.foo
chamarábar
, que chamaráfoo
.foo
tentará adquirir o bloqueio, falhará e esperará que ele seja liberado.Ok, eu traí, usando a coisa de retorno de chamada. Mas é fácil imaginar trechos de código mais complexos tendo um efeito semelhante.
3. Qual é exatamente o encadeamento comum entre os seis pontos mencionados que devo ter em mente ao verificar meu código quanto a recursos de reentrada?
Você pode sentir um problema se sua função tem / dá acesso a um recurso persistente modificável ou tem / dá acesso a uma função que cheira .
( Ok, 99% do nosso código deve cheirar, então… Consulte a última seção para lidar com isso… )
Portanto, estudando seu código, um desses pontos deve alertá-lo:
Observe que a não reentrada é viral: uma função que poderia chamar uma possível função não reentrante não pode ser considerada reentrante.
Observe também que os métodos C ++ têm cheiro porque eles têm acesso
this
; portanto, você deve estudar o código para garantir que eles não tenham nenhuma interação engraçada.4.1 Todas as funções recursivas são reentrantes?
Não.
Em casos multithread, uma função recursiva que acessa um recurso compartilhado pode ser chamada por vários threads no mesmo momento, resultando em dados incorretos / corrompidos.
Em casos de leitura única, uma função recursiva pode usar uma função não reentrante (como a infame
strtok
) ou usar dados globais sem manipular o fato de que os dados já estão em uso. Portanto, sua função é recursiva porque se chama direta ou indiretamente, mas ainda pode ser recursiva-insegura .4.2 Todas as funções de thread-safe são reentrantes?
No exemplo acima, mostrei como uma função aparentemente segura para threads não era reentrada. OK, trapacei por causa do parâmetro de retorno de chamada. Porém, existem várias maneiras de bloquear um encadeamento, adquirindo o dobro de um bloqueio não recursivo.
4.3 Todas as funções recursivas e com thread-safe são reentrantes?
Eu diria "sim" se por "recursivo" você quer dizer "recursivo-seguro".
Se você puder garantir que uma função possa ser chamada simultaneamente por vários encadeamentos e possa chamar a si mesma, direta ou indiretamente, sem problemas, ela será reentrada.
O problema está avaliando essa garantia… ^ _ ^
5. Os termos como reentrada e segurança de linha são absolutamente absolutos, ou seja, eles têm definições concretas fixas?
Acredito que sim, mas avaliar a função é seguro para threads ou reentrada pode ser difícil. É por isso que usei o termo cheiro acima: Você pode encontrar uma função que não é reentrada, mas pode ser difícil garantir que um pedaço complexo de código seja reentrante
6. um exemplo
Digamos que você tenha um objeto, com um método que precisa usar um recurso:
O primeiro problema é que, se de alguma forma essa função for chamada recursivamente (ou seja, se ela se chamar, direta ou indiretamente), o código provavelmente falhará, porque
this->p
será excluído no final da última chamada e ainda provavelmente será usado antes do final da primeira chamada.Portanto, esse código não é recursivo-seguro .
Poderíamos usar um contador de referência para corrigir isso:
Dessa forma, o código se torna seguro contra recursividade ... Mas ainda não é reentrante por causa de problemas de multithreading: devemos ter certeza de que as modificações de
c
e dep
serão feitas atomicamente, usando um mutex recursivo (nem todos os mutexes são recursivos):E, claro, tudo isso pressupõe que
lots of code
ele próprio reentre, incluindo o uso dep
.E o código acima não é nem remotamente seguro contra exceções , mas essa é outra história… ^ _ ^
7. Ei, 99% do nosso código não é reentrante!
É bem verdade para o código de espaguete. Mas se você particionar corretamente seu código, evitará problemas de reentrada.
7.1 Verifique se todas as funções não têm estado
Eles devem usar apenas os parâmetros, suas próprias variáveis locais, outras funções sem estado e retornar cópias dos dados, se retornarem.
7.2 Verifique se o seu objeto é "seguro contra recursividade"
Um método de objeto tem acesso
this
, portanto, ele compartilha um estado com todos os métodos da mesma instância do objeto.Portanto, verifique se o objeto pode ser usado em um ponto da pilha (ou seja, chamando o método A) e, em seguida, em outro ponto (ou seja, chamando o método B), sem danificar o objeto inteiro. Projete seu objeto para garantir que, ao sair de um método, ele esteja estável e correto (sem ponteiros oscilantes, sem variáveis de membros contraditórias, etc.).
7.3 Verifique se todos os seus objetos estão encapsulados corretamente
Ninguém mais deve ter acesso aos seus dados internos:
Até retornar uma referência const pode ser perigoso se o usuário recuperar o endereço dos dados, pois alguma outra parte do código pode modificá-lo sem que o código que contém a referência const seja informado.
7.4 Verifique se o usuário sabe que seu objeto não é seguro para threads
Assim, o usuário é responsável por usar mutexes para usar um objeto compartilhado entre threads.
Os objetos do STL foram projetados para não serem seguros para threads (devido a problemas de desempenho) e, portanto, se um usuário deseja compartilhar um
std::string
entre dois threads, ele deve proteger seu acesso com primitivas de simultaneidade;7.5 Certifique-se de que seu código de thread-safe seja recursivo-safe
Isso significa usar mutexes recursivos se você acredita que o mesmo recurso pode ser usado duas vezes pelo mesmo encadeamento.
fonte
"Com segurança" é definido exatamente como dita o senso comum - significa "agir corretamente, sem interferir em outras coisas". Os seis pontos que você cita expressam claramente os requisitos para conseguir isso.
As respostas para suas 3 perguntas são 3 × "não".
Todas as funções recursivas são reentrantes?
NÃO!
Duas invocações simultâneas de uma função recursiva podem facilmente estragar um ao outro, se eles acessarem os mesmos dados globais / estáticos, por exemplo.
Todas as funções de thread-safe são reentrantes?
NÃO!
Uma função é segura para threads se não funcionar corretamente se for chamada simultaneamente. Mas isso pode ser conseguido, por exemplo, usando um mutex para bloquear a execução da segunda invocação até a primeira terminar, portanto, apenas uma invocação funciona por vez. Reentrada significa executar simultaneamente sem interferir com outras invocações .
Todas as funções recursivas e com thread-safe são reentrantes?
NÃO!
Veja acima.
fonte
O segmento comum:
O comportamento está bem definido se a rotina for chamada enquanto for interrompida?
Se você tem uma função como esta:
Então não depende de nenhum estado externo. O comportamento está bem definido.
Se você tem uma função como esta:
O resultado não está bem definido em vários encadeamentos. As informações podem ser perdidas se o tempo estiver errado.
A forma mais simples de uma função reentrante é algo que opera exclusivamente nos argumentos passados e nos valores constantes. Qualquer outra coisa exige tratamento especial ou, muitas vezes, não é reentrada. E é claro que os argumentos não devem fazer referência a globais mutáveis.
fonte
Agora eu tenho que elaborar meu comentário anterior. A resposta @paercebal está incorreta. No código de exemplo, ninguém notou que o mutex que deveria ser parâmetro não foi realmente transmitido?
Eu discuto a conclusão, afirmo: para uma função ser segura na presença de simultaneidade, ela deve ser reentrada. Portanto, o concorrente-seguro (geralmente escrito por thread-safe) implica em reentrante.
Thread thread safe e reentrante não têm nada a dizer sobre argumentos: estamos falando de execução simultânea da função, que ainda pode ser insegura se parâmetros inapropriados forem usados.
Por exemplo, memcpy () é seguro para threads e reentrante (geralmente). Obviamente, não funcionará como esperado se chamado com ponteiros para os mesmos destinos de dois threads diferentes. Esse é o ponto da definição da SGI, colocando o ônus no cliente para garantir que os acessos à mesma estrutura de dados sejam sincronizados pelo cliente.
É importante entender que, em geral, é um absurdo ter uma operação segura para threads incluir os parâmetros. Se você fez alguma programação de banco de dados, entenderá. O conceito do que é "atômico" e pode ser protegido por um mutex ou alguma outra técnica é necessariamente um conceito do usuário: o processamento de uma transação em um banco de dados pode exigir várias modificações ininterruptas. Quem pode dizer quais precisam ser mantidas em sincronia, exceto o programador do cliente?
O ponto é que "corrupção" não precisa atrapalhar a memória do seu computador com gravações não serializadas: a corrupção ainda pode ocorrer mesmo se todas as operações individuais forem serializadas. Daqui resulta que, quando você pergunta se uma função é segura para thread ou reentrada, a pergunta significa para todos os argumentos separados adequadamente: o uso de argumentos acoplados não constitui um contra-exemplo.
Existem muitos sistemas de programação por aí: o Ocaml é um deles, e acho que o Python também, que possui muitos códigos não reentrantes, mas que usa uma trava global para intercalar acessos de threads. Esses sistemas não são reentrantes e não são seguros para threads ou simultâneos, eles operam com segurança simplesmente porque evitam a concorrência globalmente.
Um bom exemplo é o malloc. Não é reentrante e não é seguro para threads. Isso ocorre porque ele precisa acessar um recurso global (a pilha). Usar bloqueios não o torna seguro: definitivamente não é reentrante. Se a interface para o malloc tivesse o design adequado, seria possível torná-lo reentrante e seguro para threads:
Agora pode ser seguro, pois transfere a responsabilidade de serializar o acesso compartilhado a um único heap para o cliente. Em particular, nenhum trabalho é necessário se houver objetos de heap separados. Se uma pilha comum for usada, o cliente precisará serializar o acesso. Usando uma fechadura dentro da função não é suficiente: basta considerar um malloc bloqueando uma pilha * e, em seguida, um sinal aparece e chama malloc no mesmo ponteiro: deadlock: o sinal não pode continuar e o cliente também não pode. é interrompido.
De um modo geral, os bloqueios não tornam as coisas seguras contra threads. Na verdade, elas destroem a segurança tentando inadequadamente gerenciar um recurso pertencente ao cliente. O bloqueio deve ser feito pelo fabricante do objeto, esse é o único código que sabe quantos objetos são criados e como serão usados.
fonte
O "encadeamento comum" (trocadilho intencional !?) entre os pontos listados é que a função não deve fazer nada que afete o comportamento de quaisquer chamadas recursivas ou simultâneas para a mesma função.
Por exemplo, dados estáticos são um problema porque pertencem a todos os threads; se uma chamada modifica uma variável estática, todos os threads usam os dados modificados, afetando seu comportamento. O código de modificação automática (embora raramente seja encontrado e, em alguns casos, evitado) seria um problema, porque, embora haja vários encadeamentos, existe apenas uma cópia do código; o código também é um dado estático essencial.
Essencialmente, para ser reentrante, cada encadeamento deve poder usar a função como se fosse o único usuário, e esse não é o caso se um encadeamento puder afetar o comportamento de outro de maneira não determinística. Principalmente, isso envolve que cada thread tenha dados separados ou constantes nos quais a função trabalha.
Tudo isso dito, o ponto (1) não é necessariamente verdadeiro; por exemplo, você pode legitimamente e por design usar uma variável estática para reter uma contagem de recursão para evitar recursões excessivas ou criar um perfil para um algoritmo.
Uma função de thread-safe não precisa ser reentrada; ele pode alcançar a segurança do encadeamento ao impedir especificamente a reentrada com uma trava, e o ponto (6) diz que essa função não é reentrante. Em relação ao ponto (6), uma função que chama uma função de thread-safe que bloqueia não é segura para uso em recursão (ela será travada) e, portanto, não se diz que é reentrante, embora possa, no entanto, ser segura para simultaneidade, e ainda seria reentrante no sentido de que vários encadeamentos podem ter seus contadores de programas em uma função simultaneamente (mas não na região bloqueada). Pode ser que isso ajude a distinguir a segurança do encadeamento da reentrada (ou talvez aumente sua confusão!).
fonte
As respostas às suas perguntas "Também" são "Não", "Não" e "Não". Só porque uma função é recursiva e / ou segura para threads, não a torna reentrante.
Cada um desses tipos de função pode falhar em todos os pontos citados. (Embora eu não esteja 100% certo do ponto 5).
fonte
Os termos "thread-safe" e "reentrante" significam apenas e exatamente o que dizem suas definições. "Seguro" neste contexto significa apenas o que a definição que você cita abaixo diz.
"Seguro" aqui certamente não significa seguro, no sentido mais amplo, de que chamar uma determinada função em um determinado contexto não será totalmente sua aplicação. No total, uma função pode produzir de maneira confiável o efeito desejado em seu aplicativo multithread, mas não se qualifica como reentrante ou thread safe de acordo com as definições. Por outro lado, você pode chamar funções reentrantes de maneiras que produzam uma variedade de efeitos indesejados, inesperados e / ou imprevisíveis em seu aplicativo multithread.
A função recursiva pode ser qualquer coisa e o Re-participante tem uma definição mais forte do que o thread-safe, de modo que as respostas às suas perguntas numeradas são todas negativas.
Lendo a definição de reentrante, pode-se resumir como uma função que não modificará nada além do que você chama de modificação. Mas você não deve confiar apenas no resumo.
A programação multithread é extremamente difícil no caso geral. Saber qual parte do código reentrante é apenas parte desse desafio. A segurança da linha não é aditiva. Em vez de tentar reunir as funções reentrantes, é melhor usar um padrão geral de design seguro para threads e usar esse padrão para orientar o uso de todos os threads e recursos compartilhados no seu programa.
fonte