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:
#include <mutex>
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 função pode precisar bloquear o mesmo mutex:
void bar()
{
foo(nullptr);
}
À primeira vista, tudo parece bem ... Mas espere:
int main()
{
foo(bar);
return 0;
}
Se o bloqueio no mutex não for recursivo, eis o que acontecerá, no thread principal:
main
vai ligar foo
.
foo
adquirirá o bloqueio.
foo
chamará bar
, que chamará foo
.
- o segundo
foo
tentará adquirir o bloqueio, falhará e esperará que ele seja liberado.
- Impasse.
- Opa…
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:
- A função possui um estado (ou seja, acessar uma variável global ou mesmo uma variável de membro da classe)
- Essa função pode ser chamada por vários threads ou aparecer duas vezes na pilha enquanto o processo está em execução (ou seja, a função pode se chamar, direta ou indiretamente). Função que recebe retornos de chamada, pois os parâmetros cheiram muito.
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:
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 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:
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;
}
}
};
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 de p
serão feitas atomicamente, usando um mutex recursivo (nem todos os mutexes são recursivos):
#include <mutex>
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 pressupõe que lots of code
ele próprio reentre, incluindo o uso de p
.
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:
// 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;
}
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.