Padrões de simultaneidade e erro ocultos no código: Deadlock

Certamente, muitos ouviram, mas alguém se encontrou na prática, palavras como impasses e condições de corrida. Esses conceitos são classificados como erros no uso da simultaneidade. Se eu fizer uma pergunta sobre o que é um impasse, é muito provável que você comece a desenhar uma imagem clássica de impasse ou sua representação em pseudo-código sem qualquer dúvida. Algo assim:



Recebemos essas informações no instituto, que podem ser encontradas em livros e artigos na Internet. Esse impasse usando, por exemplo, dois mutexes, em toda a sua glória, pode ser encontrado no código. Mas, na maioria dos casos, nem tudo é tão simples, e nem todos podem ver o padrão de erro clássico no código, se ele não for apresentado da forma usual.



Considere uma classe na qual estamos interessados ​​nos métodos StartUpdate, CheckAndUpdate e Stop, C ++ é usado, o código é o mais simples possível:

std::recursive_mutex m_mutex; Future m_future; void Stop() { std::unique_lock scoped_lock(m_mutex); m_future.Wait(); // do something } void StartUpdate() { m_future.Wait(); m_future = Future::Schedule(std::bind(&Element::CheckAndUpdate, this), std::chrono::milliseconds(100); } void CheckAndUpdate() { std::unique_lock scoped_lock(m_mutex); //do something } 

No que você deve prestar atenção no código apresentado:

  1. mutex recursivo é usado. A captura repetida de um mutex recursivo não leva à expectativa apenas se essa captura ocorrer no mesmo encadeamento. Nesse caso, o número de isenções mutex deve corresponder ao número de capturas. Se tentarmos capturar um mutex recursivo que já está capturado em outro encadeamento, o encadeamento entrará no modo de espera.
  2. A função Future :: Schedule é iniciada (em n milissegundos) em um thread separado que o retorno de chamada passou para ele

Agora analisamos todas as informações recebidas e compomos uma imagem:



Considerando os dois fatos apresentados acima, não é difícil concluir que uma tentativa de capturar um mutex recursivo em uma das funções levará à expectativa de liberação do mutex se ele já tiver sido capturado em outra função, pois o retorno de chamada CheckAndUpdate sempre é executado em um thread separado.

À primeira vista, não há nada suspeito em relação ao impasse. Mas, para ser mais próximo, tudo se resume à nossa imagem clássica. Quando o objeto funcional começa a executar, capturamos implicitamente o recurso m_future, o retorno de chamada diretamente
associado ao m_future:



A sequência de ações que levam ao impasse é a seguinte:

  1. Está planejado executar o CheckAndUpdate, mas o retorno de chamada não inicia imediatamente, após n milissegundos.
  2. O método Stop é chamado e, em seguida, é iniciado: tentamos capturar o mutex - o recurso é um capturado, começamos a esperar que m_future seja concluído - o objeto ainda não foi chamado, estamos aguardando.
  3. A execução do CheckAndUpdate começa: tentamos capturar o mutex - não podemos, o recurso já está capturado por outro encadeamento, estamos aguardando liberação.

Isso é tudo: o encadeamento que faz a chamada Stop aguarda a conclusão do CheckAndUpdate, e o outro encadeamento, por sua vez, não pode continuar trabalhando até pegar o mutex que já foi capturado pelo encadeamento mencionado anteriormente. É um impasse clássico. Metade do trabalho está concluído - a causa do problema foi descoberta.

Agora um pouco sobre como corrigi-lo.
Abordagem 1
O procedimento para capturar recursos deve ser o mesmo, para evitar conflitos. Ou seja, você precisa ver se é possível alterar a ordem de captura de recursos no método Stop. Como aqui o caso de impasse não é totalmente óbvio e não há captura explícita do recurso m_future no CheckAndUpdate, decidimos pensar em outra solução para evitar o retorno de erro no futuro.

Abordagem 2
  1. Verifique se você pode optar por não usar o mutex em CheckAndUpdate.
  2. Como usamos o mecanismo de sincronização, restringimos o acesso a alguns recursos. Talvez seja o suficiente para você refazer esses recursos em atômicos (como tínhamos), cujo acesso já é seguro para threads.
  3. Descobriu-se que as variáveis, cujo acesso era limitado, podem ser facilmente convertidas em atômicas, portanto o mutex mencionado é excluído com sucesso.


Aqui está um exemplo tão simples com um impasse não óbvio que se reduz facilmente ao padrão desse erro. Finalmente, desejo que você escreva um código confiável e seguro para threads!

Source: https://habr.com/ru/post/pt442448/


All Articles