Threadsafe vs reentrante

89

Recentemente, fiz uma pergunta com o título "Is malloc thread safe?" , e dentro disso perguntei: "Malloc é reentrante?"

Fiquei com a impressão de que todos os reentrantes são seguros para discussão.

Esta suposição está errada?

Alphaneo
fonte

Respostas:

42

Funções re-entrantes não dependem de variáveis ​​globais que são expostas nos cabeçalhos da biblioteca C .. pegue strtok () vs strtok_r () por exemplo em C.

Algumas funções precisam de um local para armazenar um 'trabalho em andamento', funções reentrantes permitem que você especifique este ponteiro dentro do armazenamento do próprio thread, não em um global. Uma vez que este armazenamento é exclusivo para a função de chamada, ele pode ser interrompido e reinserido (reentrante) e, uma vez que na maioria dos casos, a exclusão mútua além do que a função implementa não é necessária para que isso funcione, eles são frequentemente considerados discussão segura . Isso não é, entretanto, garantido por definição.

errno, no entanto, é um caso ligeiramente diferente em sistemas POSIX (e tende a ser estranho em qualquer explicação de como tudo isso funciona) :)

Resumindo, reentrant geralmente significa thread safe (como em "use a versão reentrant dessa função se estiver usando threads"), mas thread safe nem sempre significa reentrante (ou o contrário). Quando você está olhando para thread-safety, a simultaneidade é o que você precisa pensar. Se você tiver que fornecer um meio de bloqueio e exclusão mútua para usar uma função, então a função não é inerentemente thread-safe.

Mas, nem todas as funções precisam ser examinadas para qualquer um. malloc()não precisa ser reentrante, não depende de nada fora do escopo do ponto de entrada para qualquer thread (e é seguro para thread).

Funções que retornam valores alocados estaticamente não são thread-safe sem o uso de um mutex, futex ou outro mecanismo de bloqueio atômico. No entanto, eles não precisam ser reentrantes se não forem interrompidos.

ie:

Então, como você pode ver, ter vários threads usando isso sem algum tipo de bloqueio seria um desastre ... mas não tem o propósito de ser reentrado. Você encontrará isso quando a memória alocada dinamicamente for um tabu em alguma plataforma embarcada.

Na programação puramente funcional, reentrant muitas vezes não implica thread-safe, dependeria do comportamento de funções definidas ou anônimas passadas para o ponto de entrada da função, recursão, etc.

A melhor maneira de colocar 'thread safe' é segura para acesso simultâneo , o que ilustra melhor a necessidade.

Tim Post
fonte
2
Reentrant não implica thread-safe. Funções puras implicam segurança de thread.
Julio Guerra
Ótima resposta, Tim. Apenas para esclarecer, meu entendimento do seu "freqüentemente" é que thread-safe não implica reentrante, mas também reentrante não implica thread-safe. Você seria capaz de encontrar um exemplo de uma função reentrante que não seja thread-safe?
Riccardo
@ Tim Post "Resumindo, reentrante geralmente significa thread-safe (como em" use a versão reentrante dessa função se estiver usando threads "), mas thread-safe nem sempre significa reentrante." qt diz o oposto: "Conseqüentemente, uma função thread-safe é sempre reentrante, mas uma função reentrante nem sempre é thread-safe."
4pie0 de
e a wikipedia diz ainda outra coisa: "Esta definição de reentrada difere daquela de thread-safety em ambientes multi-threaded. Uma sub-rotina reentrante pode atingir thread-safety, [1] mas ser reentrante sozinho pode não ser suficiente para ser thread-safe em todas as situações. Por outro lado, o código thread-safe não precisa ser necessariamente reentrante (...) "
4pie0
@Riccardo: Funções sincronizadas por meio de variáveis ​​voláteis, mas não barreiras de memória total para uso com manipuladores de sinal / interrupção são geralmente reentrantes, mas seguras com thread.
doynax,
79

TL; DR: uma função pode ser reentrante, thread-safe, ambas ou nenhuma.

Os artigos da Wikipedia para thread-safety e reentrância valem a pena ler. Aqui estão algumas citações:

Uma função é thread-safe se:

ele apenas manipula estruturas de dados compartilhadas de uma maneira que garante uma execução segura por vários threads ao mesmo tempo.

Uma função é reentrante se:

ele pode ser interrompido em qualquer ponto durante sua execução e então seguramente chamado novamente ("reinserido") antes que suas invocações anteriores completem a execução.

Como exemplos de possíveis reentradas, a Wikipedia dá o exemplo de uma função projetada para ser chamada por interrupções do sistema: suponha que já esteja em execução quando outra interrupção ocorrer. Mas não pense que você está seguro apenas porque não codifica com interrupções do sistema: você pode ter problemas de reentrada em um programa de thread único se usar callbacks ou funções recursivas.

A chave para evitar confusão é que reentrant se refere a apenas um thread em execução. É um conceito da época em que não existiam sistemas operacionais multitarefa.

Exemplos

(Ligeiramente modificado dos artigos da Wikipedia)

Exemplo 1: não thread-safe, não reentrante

Exemplo 2: thread-safe, não reentrante

Exemplo 3: não thread-safe, reentrante

Exemplo 4: thread-safe, reentrant

MiniQuark
fonte
10
Eu sei que não devo comentar apenas para agradecer, mas esta é uma das melhores ilustrações que mostram as diferenças entre as funções reentrant e thread safe. Em particular, você usou termos claros muito concisos e escolheu uma excelente função de exemplo para distinguir entre as 4 categorias. Então, obrigado!
Ryyker
11
Parece-me que o exemplo 3 não é reentrante: se um manipulador de sinal, interrompendo depois t = *x, chama swap(), então tserá substituído, levando a resultados inesperados.
rom1v
1
@ SandBag_1996, vamos considerar uma chamada para swap(5, 6)ser interrompido por a swap(1, 2). Depois t=*x, s=t_originale t=5. Agora, após a interrupção, s=5e t=1. No entanto, antes que o segundo swapretorne, ele irá restaurar o contexto, fazendo t=s=5. Agora, voltamos ao primeiro swapcom t=5 and s=t_originale continuamos depois t=*x. Portanto, a função parece ser reentrante. Lembre-se de que cada chamada obtém sua própria cópia de salocada na pilha.
urnonav
4
@ SandBag_1996 A suposição é que, se a função for interrompida (em qualquer ponto), ela apenas será chamada novamente e esperamos até que seja concluída antes de continuar a chamada original. Se algo mais acontecer, então é basicamente multithreading e esta função não é segura para thread. Suponha que a função faça ABCD, só aceitamos coisas como AB_ABCD_CD, ou A_ABCD_BCD, ou mesmo A__AB_ABCD_CD__BCD. Como você pode verificar, o exemplo 3 funcionaria bem sob essas premissas, portanto, é reentrante. Espero que isto ajude.
MiniQuark
1
@ SandBag_1996, mutex realmente o tornaria não reentrante. A primeira chamada bloqueia o mutex. Em seguida, vem a segunda invocação - impasse.
urnonav
56

Depende da definição. Por exemplo Qt usa o seguinte:

  • Uma função thread-safe * pode ser chamada simultaneamente a partir de vários threads, mesmo quando as invocações usam dados compartilhados, porque todas as referências aos dados compartilhados são serializadas.

  • UMA função reentrante também pode ser chamada simultaneamente a partir de vários encadeamentos, mas apenas se cada chamada usar seus próprios dados.

Portanto, um função thread-safe é sempre reentrante, mas uma função reentrante nem sempre é thread-safe.

Por extensão, uma classe é considerada reentrante se suas funções-membro puderem ser chamadas com segurança a partir de vários threads, desde que cada thread use uma instância diferente da classe. A classe é segura para thread se suas funções de membro puderem ser chamadas com segurança de vários threads, mesmo se todos os threads usarem a mesma instância da classe.

mas também alertam:

Observação: a terminologia no domínio multithreading não é totalmente padronizada. POSIX usa definições de reentrant e thread-safe que são um pouco diferentes para suas APIs C. Ao usar outras bibliotecas de classes C ++ orientadas a objetos com Qt, certifique-se de que as definições sejam compreendidas.

Georg Schölly
fonte
2
Esta definição de reentrante é muito forte.
qweruiop
Uma função é reentrante e thread-safe se não usar nenhuma var global / estática. Thread-safe: quando muitos threads executam sua função ao mesmo tempo, há alguma corrida? Se você usar var global, use lock para protegê-lo. por isso é thread-safe. reentrante: se ocorrer um sinal durante a execução de sua função, e chamar sua função no sinal novamente, é seguro ??? nesse caso, não há vários threads. É melhor que você não use nenhuma var estática / global para torná-la reentrante, ou como no exemplo 3.
keniee van