O que exatamente é uma function reentrante?

Na maioria das vezes , a definição de reinput é citada na Wikipedia :

Um programa ou rotina de computador é descrito como reentrante, se puder ser chamado com segurança novamente antes que sua invocação anterior tenha sido concluída (isto é, possa ser executada com segurança simultaneamente). Para ser reentrante, um programa de computador ou rotina:

  1. Não deve conter dados não constantes estáticos (ou globais).
  2. Não deve retornar o endereço para dados não constantes estáticos (ou globais).
  3. Deve funcionar apenas nos dados fornecidos pelo chamador.
  4. Não deve confiar em bloqueios para resources singleton.
  5. Não deve modificar seu próprio código (a menos que esteja executando em seu próprio armazenamento de encadeamento exclusivo)
  6. 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 ao mesmo tempo , significa sempre que ele é reentrante?

O que exatamente é o fio comum entre os seis pontos mencionados que devo ter em mente ao verificar meu código para resources reentrantes?

Além disso,

  1. Todas as funções recursivas são reentrantes?
  2. Todas as funções thread-safe são reentrantes?
  3. Todas as funções recursivas e thread-safe são reentrantes?

Ao escrever essa questão, uma coisa vem à mente: os termos como reinput e segurança de rosca são absolutos, ou seja, eles têm definições fixas de concreto? Pois, se não forem, essa questão não é muito significativa.

1. Como é definido com segurança ?

Semanticamente. Nesse caso, esse não é um termo definido com dificuldade. Significa apenas “Você pode fazer isso, sem risco”.

2. Se um programa puder ser executado com segurança ao mesmo tempo, significa sempre que ele é reentrante?

Não.

Por exemplo, vamos ter uma function C ++ que leve um bloqueio e um retorno de chamada como parâmetro:

#include  typedef void (*callback)(); std::mutex m; void foo(callback f) { m.lock(); // use the resource protected by the mutex if (f) { f(); } // use the resource protected by the mutex m.unlock(); } 

Outra function pode precisar bloquear o mesmo mutex:

 void bar() { foo(nullptr); } 

À primeira vista, tudo parece ok … Mas espere:

 int main() { foo(bar); return 0; } 

Se o bloqueio no mutex não for recursivo, então aqui está o que acontecerá no thread principal:

  1. main chamará o foo .
  2. foo irá adquirir o bloqueio.
  3. foo vai ligar para o bar , que vai chamar foo .
  4. o segundo foo tentará adquirir o bloqueio, falhar e esperar que ele seja liberado.
  5. Impasse.
  6. Opa …

Ok, eu trapaceei, usando a coisa de callback. Mas é fácil imaginar pedaços de código mais complexos com um efeito semelhante.

3. Qual é exatamente o fio comum entre os seis pontos mencionados que devo ter em mente ao verificar meu código para resources reentrantes?

Você pode cheirar um problema se sua function tiver / der access a um recurso persistente modificável ou tiver / der access a uma function que cheira .

( Ok, 99% do nosso código deve cheirar, então… Veja a última seção para lidar com isso… )

Então, estudando seu código, um desses pontos deve alertar você:

  1. A function tem um estado (ou seja, acessar uma variável global ou até mesmo uma variável de membro de class)
  2. Essa function pode ser chamada por vários threads, ou pode aparecer duas vezes na pilha enquanto o processo está sendo executado (isto é, a function pode se chamar, direta ou indiretamente). Função recebendo callbacks como parâmetros cheiram muito.

Note que a não reentrância é viral: Uma function que poderia chamar uma possível function não reentrante não pode ser considerada reentrante.

Note, também, que os methods C + + cheiram porque eles têm access a this , então você deve estudar o código para ter certeza de que eles não têm nenhuma interação engraçada.

4.1. Todas as funções recursivas são reentrantes?

Não.

Em casos com multithread, uma function recursiva acessando resources compartilhados poderia ser chamada por vários threads no mesmo momento, resultando em dados corrompidos / ruins.

Em casos singlethreaded, uma function recursiva poderia usar uma function não reentrante (como strtok infame), ou usar dados globais sem lidar com o fato dos dados já estarem em uso. Portanto, sua function é recursiva porque se chama direta ou indiretamente, mas ainda pode ser recursiva-insegura .

4.2. Todas as funções thread-safe são reentrantes?

No exemplo acima, mostrei como uma function aparentemente thread-safe não era reentrante. Ok eu enganei por causa do parâmetro de retorno de chamada. Mas, há várias maneiras de travar um thread fazendo com que ele adquira duas vezes um bloqueio não recursivo.

4.3. Todas as funções recursivas e thread-safe são reentrantes?

Eu diria “sim” se por “recursivo” você quer dizer “recursivo-seguro”.

Se você pode garantir que uma function pode ser chamada simultaneamente por múltiplos threads, e pode chamar a si mesma, direta ou indiretamente, sem problemas, então ela é reentrante.

O problema é avaliar essa garantia… ^ _ ^

5. Os termos como reinput e segurança de rosca são absolutos, ou seja, eles têm definições fixas de concreto?

Eu acredito que eles têm, mas, em seguida, avaliar uma function é thread-safe ou reentrant pode ser difícil. É por isso que usei o termo cheiro acima: Você pode achar que uma function não é reentrante, mas pode ser difícil ter certeza de que uma parte complexa do código é reentrante

6. Um exemplo

Digamos que você tenha um object com um método que precise usar resources:

 struct MyStruct { P * p; void foo() { if (this->p == nullptr) { this->p = new P(); } // lots of code, some using this->p if (this->p != nullptr) { delete this->p; this->p = nullptr; } } }; 

O primeiro problema é que se de alguma forma esta function é chamada recursivamente (ou seja, esta function chama a si mesma, direta ou indiretamente), o código provavelmente falhará, porque this->p será deletado no final da última chamada, e provavelmente ainda será usado antes do final da primeira chamada.

Portanto, esse código não é seguro recursivo .

Poderíamos usar um contador de referência para corrigir isso:

 struct MyStruct { size_t c; P * p; void foo() { if (c == 0) { this->p = new P(); } ++c; // lots of code, some using this->p --c; if (c == 0) { delete this->p; this->p = nullptr; } } }; 

Desta forma, o código torna-se seguro recursivo … Mas ainda não é reentrante por causa de questões multithreading: Devemos ter certeza de que as modificações de c e de p serão feitas atomicamente, usando um mutex recursivo (nem todas as mutexes são recursivas):

 #include  struct MyStruct { std::recursive_mutex m; size_t c; P * p; void foo() { m.lock(); if (c == 0) { this->p = new P(); } ++c; m.unlock(); // lots of code, some using this->p m.lock(); --c; if (c == 0) { delete this->p; this->p = nullptr; } m.unlock(); } }; 

E, claro, tudo isso assume que o lots of code é reentrante, incluindo o uso de p .

E o código acima não é nem remotamente seguro para exceção , mas isso é outra história… ^ _ ^

7. Hey 99% do nosso código não é reentrante!

É bem verdade para o código de espaguete. Mas se você particionar corretamente seu código, você evitará problemas de reentrância.

7.1. Certifique-se de que todas as funções não possuem estado

Eles devem usar apenas os parâmetros, suas próprias variables ​​locais, outras funções sem estado e retornar cópias dos dados, caso retornem.

7.2. Certifique-se de que seu object seja “seguro recursivo”

Um método de object tem access a this , portanto, ele compartilha um estado com todos os methods da mesma instância do object.

Portanto, verifique se o object pode ser usado em um ponto da pilha (ou seja, método de chamada A) e, em seguida, em outro ponto (ou seja, método de chamada B), sem corromper o object inteiro. Projete seu object para certificar-se de que ao sair de um método, o object esteja estável e correto (sem pointers pendentes, sem variables ​​de membro em contradição, etc.).

7.3. Certifique-se de que todos os seus objects estejam corretamente encapsulados

Ninguém mais deveria ter access aos seus dados internos:

  // bad int & MyObject::getCounter() { return this->counter; } // good int MyObject::getCounter() { return this->counter; } // good, too void MyObject::getCounter(int & p_counter) { p_counter = this->counter; } 

Mesmo retornar uma referência const poderia ser perigoso se o uso recuperasse o endereço dos dados, já que alguma outra parte do código poderia modificá-lo sem que o código contendo a referência const fosse contada.

7.4. Verifique se o usuário sabe que seu object não é thread-safe

Assim, o usuário é responsável por usar mutexes para usar um object compartilhado entre threads.

Os objects do STL são projetados para não serem thread-safe (devido a problemas de desempenho) e, portanto, se um usuário desejar compartilhar um std::string entre dois threads, o usuário deve proteger seu access com primitivas de simultaneidade;

7,5. Certifique-se de que o código seguro para thread é seguro recursivo

Isso significa usar mutexes recursivas se você acredita que o mesmo recurso pode ser usado duas vezes pelo mesmo thread.

“Com segurança” é definido exatamente como o senso comum determina – significa “fazer as coisas 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 function recursiva podem facilmente atrapalhar uma a outra, se elas acessarem os mesmos dados globais / estáticos, por exemplo.


Todas as funções thread-safe são reentrantes?

NÃO!

Uma function é thread-safe se não funcionar mal se for chamada simultaneamente. Mas isso pode ser alcançado, por exemplo, usando um mutex para bloquear a execução da segunda chamada até que a primeira seja concluída, portanto, apenas uma chamada funciona por vez. Reentrância significa executar concorrentemente sem interferir com outras invocações .


Todas as funções recursivas e thread-safe são reentrantes?

NÃO!

Veja acima.

O fio comum:

O comportamento é bem definido se a rotina é chamada enquanto é interrompida?

Se você tem uma function como esta:

 int add( int a , int b ) { return a + b; } 

Então, não depende de nenhum estado externo. O comportamento é bem definido.

Se você tem uma function como esta:

 int add_to_global( int a ) { return gValue += a; } 

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 function de reinput é algo que opera exclusivamente sobre os argumentos passados ​​e valores constantes. Qualquer outra coisa requer tratamento especial ou, muitas vezes, não é reentrante. E, claro, os argumentos não devem referenciar globals mutáveis.

Agora tenho que elaborar meu comentário anterior. A resposta do @paercebal está incorreta. No código de exemplo, ninguém percebeu que o mutex, que deveria ser um parâmetro, não foi realmente passado?

Discuto a conclusão, afirmo: para que uma function seja segura na presença de concorrência, ela deve ser reentrante. Portanto, o uso simultâneo de segurança (geralmente escrito seguro para thread) implica em reinput.

Nem thread safe nem re-entrant têm algo a dizer sobre argumentos: estamos falando sobre a execução simultânea da function, que ainda pode ser insegura se parâmetros inapropriados forem usados.

Por exemplo, memcpy () é thread-safe e re-entrant (geralmente). Obviamente, ele não funcionará como esperado se for chamado com pointers para os mesmos destinos de dois encadeamentos diferentes. Esse é o ponto da definição da SGI, colocando o ônus do cliente para garantir que os accesss à mesma estrutura de dados sejam sincronizados pelo cliente.

É importante entender que, em geral, é um absurdo ter uma operação thread-safe que inclua os parâmetros. Se você já fez alguma programação de database, você entenderá. O conceito do que é “atômico” e pode ser protegido por um mutex ou alguma outra técnica é necessariamente um conceito de usuário: processar uma transação em um database pode requerer múltiplas modificações não interrompidas. Quem pode dizer quais precisam ser mantidos em sincronia, mas o programador cliente?

A questão é que “corrupção” não precisa estar atrapalhando a memory do seu computador com gravações não serializadas: a corrupção ainda pode ocorrer mesmo se todas as operações individuais forem serializadas. Segue-se que quando você está perguntando se uma function é thread-safe, ou re-entrant, a pergunta significa para todos os argumentos separados apropriadamente: usar argumentos acoplados não constitui um contra-exemplo.

Existem muitos sistemas de programação: o Ocaml é um deles, e também o Python, que tem um monte de código não reentrante, mas que usa um bloqueio global para intercalar accesss a threads. Esses sistemas não são reentrantes e não são thread-safe ou concorrentes-safe, eles operam com segurança simplesmente porque eles impedem a simultaneidade globalmente.

Um bom exemplo é o malloc. Não é re-entrante e não thread-safe. Isso ocorre porque ele precisa acessar um recurso global (o heap). Usar bloqueios não é seguro: definitivamente não é reentrante. Se a interface para o malloc tivesse um design adequado, seria possível torná-lo reentrante e seguro para thread:

 malloc(heap*, size_t); 

Agora, pode ser seguro, porque transfere a responsabilidade de serializar o access compartilhado a um único heap para o cliente. Em particular, nenhum trabalho é necessário se houver objects heap separados. Se um heap comum for usado, o cliente precisará serializar o access. Usar um bloqueio dentro da function não é suficiente: basta considerar um malloc bloqueando um heap * e então um sinal aparece e chama malloc no mesmo ponteiro: deadlock: o sinal não pode continuar, e o cliente também não pode porque está interrompido.

De um modo geral, bloqueios não fazem coisas thread-safe … eles realmente destruir segurança por inadequadamente tentando gerenciar um recurso que é propriedade do cliente. O bloqueio tem que ser feito pelo fabricante do object, que é o único código que sabe quantos objects são criados e como eles serão usados.

O “fio comum” (trocadilho intencional !?) entre os pontos listados é que a function não deve fazer nada que afete o comportamento de qualquer chamada recursiva ou concorrente para a mesma function.

Então, por exemplo, dados estáticos são um problema porque são de propriedade de todos os threads; Se uma chamada modificar uma variável estática, todos os threads usarão os dados modificados, afetando o comportamento deles. O código de modificação automática (embora raramente encontrado e, em alguns casos, impedido) seria um problema, porque embora haja vários segmentos, existe apenas uma cópia do código; o código também é essencial para dados estáticos.

Essencialmente para ser reentrante, cada thread deve poder usar a function como se fosse o único usuário, e esse não é o caso se um thread puder afetar o comportamento de outro de uma maneira não determinística. Principalmente, isso envolve cada thread com dados separados ou constantes nos quais a function funciona.

Tudo o que disse, o ponto (1) não é necessariamente verdade; por exemplo, você pode legitimamente e por design usar uma variável estática para reter uma contagem de recursion para proteger contra recursion excessiva ou para criar um perfil de um algoritmo.

Uma function thread-safe não precisa ser reentrante; ele pode atingir a segurança do segmento, impedindo especificamente a reinput com uma trava, e o ponto (6) diz que tal function não é reentrante. No que diz respeito ao ponto (6), uma function que chama uma function thread-safe que bloqueia não é segura para uso em recursion (será dead-lock) e, portanto, não é reinput, embora possa ser segura para simultaneidade, e ainda seria reentrante no sentido de que vários encadeamentos podem ter seus contadores de programa em tal function simultaneamente (mas não com a região bloqueada). Isso pode ajudar a distinguir a segurança de thread da reinput (ou talvez acrescente à sua confusão!).

As respostas às suas perguntas “Além disso” são “Não”, “Não” e “Não”. Só porque uma function é recursiva e / ou thread segura, ela não a torna reentrante.

Cada um desses tipos de function pode falhar em todos os pontos citados. (Embora eu não esteja 100% certo do ponto 5).

Os termos “Segmento seguro” e “reentrância” significam apenas e exatamente o que suas definições dizem. “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 function em um determinado contexto não irá cobrir totalmente sua aplicação. Em conjunto, uma function pode produzir com segurança um efeito desejado em seu aplicativo multiencadeado, mas não se qualifica como reentrante ou thread safe de acordo com as definições. Da mesma forma, você pode chamar funções de reentrância de maneiras que produzirão uma variedade de efeitos indesejados, inesperados e / ou imprevisíveis em seu aplicativo multiencadeado.

Função recursiva pode ser qualquer coisa e Re-entrante tem uma definição mais forte do que thread-safe, então as respostas para as suas perguntas numeradas são todas negativas.

Lendo a definição de reentrante, pode-se resumir isso como significando uma function que não modificará nada além do que você chama para modificar. Mas você não deve confiar apenas no resumo.

A programação multi-threaded é extremamente difícil no caso geral. Saber qual parte do código reentrante é apenas uma parte desse desafio. A segurança do fio não é aditiva. Em vez de tentar reunir funções reentrantes, é melhor usar um padrão geral de design seguro para thread e usar esse padrão para guiar o uso de cada thread e resources compartilhados no programa.