O que você procura ao depurar deadlocks?

25

Recentemente, tenho trabalhado em projetos que usam fortemente o encadeamento. Eu acho que estou bem em projetá-los; use o design sem estado, tanto quanto possível, bloqueie o acesso a todos os recursos que mais de um encadeamento precisa, etc. Minha experiência em programação funcional ajudou imensamente.

No entanto, ao ler o código de thread de outras pessoas, fico confuso. Estou depurando um impasse no momento e, como o estilo e o design de codificação são diferentes do meu estilo pessoal, estou tendo dificuldades para ver possíveis condições de impasse.

O que você procura ao depurar deadlocks?

Michael K
fonte
Estou perguntando isso aqui, em vez de SO, porque quero dicas mais gerais sobre a depuração de conflitos, não uma resposta específica para o meu problema.
Michael K
As estratégias em que posso pensar estão registrando (como várias outras salientaram), examinando o gráfico de deadlock de quem está esperando por um cadeado mantido por quem (consulte stackoverflow.com/questions/3483094/… para mais informações). ponteiros) e anotações de bloqueio (consulte clang.llvm.org/docs/ThreadSafetyAnalysis.html ). Mesmo que não seja o seu código, tente convencer o autor a adicionar anotações - ele provavelmente encontrará bugs e os corrigirá (possivelmente incluindo o seu) no processo.
Don Hatch

Respostas:

23

Se a situação for um conflito real (ou seja, dois encadeamentos mantêm dois bloqueios diferentes, mas pelo menos um encadeamento deseja um bloqueio que o outro encadea), será necessário abandonar todas as pré-concepções de como os encadeamentos ordenam o bloqueio. Supor nada. Você pode remover todos os comentários do código que está visualizando, pois esses comentários podem fazer com que você acredite em algo que não é verdadeiro. É difícil enfatizar isso o suficiente: não assuma nada.

Depois disso, determine quais bloqueios serão retidos enquanto um encadeamento tenta bloquear outra coisa. Se puder, assegure-se de que uma linha seja desbloqueada na ordem inversa do bloqueio. Ainda melhor, assegure-se de que uma linha segure apenas uma trava por vez.

Trabalhe meticulosamente na execução de um thread e examine todos os eventos de bloqueio. Em cada bloqueio, determine se um encadeamento retém outros bloqueios e, em caso afirmativo, em que circunstâncias outro encadeamento, executando um caminho de execução semelhante, pode chegar ao evento de bloqueio em consideração.

Certamente é possível que você não encontre o problema antes de ficar sem tempo ou dinheiro.

Bruce Ediger
fonte
4
+1 Uau, isso é pessimista ... não é verdade, no entanto. É certo que você não consegue encontrar todos os erros. Obrigado pelas sugestões!
Michael K
Bruce, sua caracterização de "verdadeiro impasse" é surpreendente para mim. Eu pensei que um impasse entre dois threads é quando cada um está esperando por um bloqueio que o outro mantém. Sua definição parece incluir também o caso de um encadeamento, enquanto mantém um bloqueio, aguarda para adquirir um segundo bloqueio atualmente retido por um encadeamento diferente. Isso não parece um impasse para mim; é isso??
Don Hatch
@ DonHatch - eu fraseado. A situação que você descreve não é um impasse. Eu esperava transmitir a confusão de depurar uma situação que inclui uma trava de retenção de segmento A e depois tentar obter o bloqueio B, enquanto o segmento que segura o bloqueio B está tentando obter o bloqueio A. Talvez. Ou talvez a situação seja muito mais complicada. Você só precisa manter uma mente muito aberta sobre a ordem de aquisição do bloqueio. Examine todas as suposições. Não confie em nada.
Bruce Ediger
+1 sugerindo a leitura cuidadosa do código e a verificação isolada de todas as operações de bloqueio. Muito mais fácil olhar para um gráfico complexo, examine cuidadosamente um único nó do que tentar ver tudo de uma só vez. Quantas vezes encontrei o problema apenas olhando o código e executando diferentes cenários na minha cabeça.
Newtopian 29/09/16
11
  1. Como outros já disseram ... se você puder obter informações úteis para o log, tente primeiro, pois é a coisa mais fácil de fazer.

  2. Identifique os bloqueios envolvidos. Mude todos os mutex / semáforos que esperam eternamente para esperas cronometradas ... algo ridiculamente longo, como 5 minutos. Registre o erro quando atingir o tempo limite. Isso pelo menos indicará você na direção de um dos bloqueios envolvidos no problema. Dependendo da variabilidade do tempo, você pode ter sorte e encontrar os dois bloqueios após algumas execuções. Use os códigos / condições de falha de função para registrar um rastreamento de pseudo-pilha após a espera temporizada falhar para identificar como você chegou lá em primeiro lugar. Isso deve ajudá-lo a identificar o segmento envolvido no problema.

  3. Outra coisa que você pode tentar é criar uma biblioteca de wrapper em torno de seus serviços de mutex / semáforo. Acompanhe quais threads têm cada mutex e quais threads estão esperando no mutex. Crie um encadeamento de monitor que verifique por quanto tempo os encadeamentos estão bloqueando. Acione com duração razoável e despeje as informações de estado que você está rastreando.

Em algum momento, será necessária uma inspeção simples e antiga do código.

Pemdas
fonte
6

O primeiro passo (como Péter diz) é o log. Embora, na minha experiência, isso seja frequentemente problemático. No processamento paralelo pesado, isso geralmente não é possível. Eu tive que depurar algo semelhante com uma rede neural uma vez, que processou 100k de nós por segundo. O erro ocorreu apenas depois de várias horas e até uma única linha de saída diminuiu tanto as coisas que levaria dias. Se o registro for possível, concentre-se menos nos dados, mas mais no fluxo do programa, até que você saiba em qual parte isso acontece. Apenas uma linha simples no início de cada função e, se você encontrar a função correta, divida-a em pedaços menores.

Outra opção é remover partes do código e dados para localizar o bug. Talvez até escreva um pequeno programa que leve apenas algumas das classes e execute apenas os testes mais básicos (ainda em vários segmentos, é claro). Remova tudo que estiver relacionado à GUI, por exemplo, qualquer saída sobre o estado de processamento real. (Eu achei a interface do usuário a fonte do bug com bastante frequência)

No seu código, tente seguir o fluxo lógico completo de controle entre inicializar o bloqueio e liberá-lo. Um erro comum pode ser bloquear no início de uma função, desbloquear no final, mas ter uma declaração de retorno condicional em algum lugar no meio. Exceções também podem impedir a liberação.

thorsten müller
fonte
"Exceções podem impedir a liberação" -> Tenho pena de linguagens que não possuem variáveis ​​de escopo: /
Matthieu M.
1
@ Matthieu: Ter variáveis ​​de escopo e realmente usá-las adequadamente pode ser duas coisas diferentes. E ele pediu possíveis problemas em geral, sem mencionar um idioma específico. Portanto, isso é uma coisa que pode influenciar o fluxo de controle.
22611 Thorsten
3

Meus melhores amigos foram declarações de impressão / log em lugares interessantes do código. Isso geralmente me ajuda a entender melhor o que realmente está acontecendo dentro do aplicativo, sem interromper o tempo entre diferentes threads, o que poderia impedir a reprodução do bug.

Se isso falhar, meu único método restante é observar o código e tentar criar um modelo mental dos vários threads e interações, e tentar pensar em possíveis maneiras malucas de alcançar o que aparentemente aconteceu :-) Mas eu não considero-me um matador de impasse muito experiente. Espero que outros possam dar melhores idéias, das quais também posso aprender :-)

Péter Török
fonte
1
Eu depurei um par de cadeados mortos hoje. O truque era envolver pthread_mutex_lock () com uma macro que imprime a função, o número da linha, o nome do arquivo e o nome da variável mutex (ao tokenizá-la) antes e depois da aquisição do bloqueio. Faça o mesmo para pthread_mutex_unlock () também. Quando vi que meu tópico congelou, eu apenas tive que olhar para as duas últimas mensagens, havia dois tópicos tentando bloquear, mas nunca terminando! Agora tudo o que resta é adicionar um mecanismo para alternar isso em tempo de execução. :-)
Plumenator
3

Primeiro de tudo, tente obter o autor desse código. Ele provavelmente terá a ideia do que escreveu. mesmo que vocês dois não consigam identificar o problema apenas conversando, pelo menos você pode sentar com ele para identificar a parte do impasse, que será muito mais rápida do que você entende o código sem ajuda.

Caso contrário, como Péter Török disse, o registro é provavelmente o caminho. Até onde eu sei, o Debugger fez um trabalho ruim no ambiente com vários threads. tente localizar onde está a trava, obtenha um conjunto dos recursos que estão aguardando e em que condição ocorre a condição de corrida.

Zekta Chan
fonte
não, o log é seu inimigo aqui - quando você faz logon lento, altera o comportamento do programa para o ponto em que é fácil obter um programa que funcione perfeitamente com o log habilitado, mas que trava quando o log é desativado. É o mesmo tipo de problema que você teria ao executar um programa em uma única CPU, e não em uma CPU multicore.
Gbjbaanb
@gbjbaanb, acho que dizer que é seu inimigo é muito duro. Talvez seja correto dizer que é seu melhor amigo, que decepciona você de vez em quando. Concordo com várias outras pessoas nesta página que dizem que o log é um bom primeiro passo a ser seguido, após o exame do código falhar - muitas vezes (na verdade, na maioria das vezes, na minha experiência) uma estratégia simples de log localiza o problema facilmente e pronto. Caso contrário, de qualquer modo, recorra a outros métodos, mas não acho que seja um bom conselho evitar tentar a melhor ferramenta para o trabalho, apenas porque nem sempre é útil.
Don Hatch
0

Esta pergunta me atrai;) Antes de tudo, considere-se com sorte, pois você conseguiu reproduzir o problema de forma consistente a cada execução. Se você receber a mesma exceção com o mesmo rastreamento de pilha de cada vez, deverá ser bastante direto. Caso contrário, não confie tanto no stacktrace, apenas monitore o acesso aos objetos globais e seu estado muda durante a execução.


fonte
0

Se você precisar depurar impasses, já está com problemas. Como regra, use bloqueios pelo menor tempo possível - ou, se não for possível, de jeito nenhum. Qualquer situação em que você trava e depois passa para um código não trivial deve ser evitada.

É claro que depende do seu ambiente de programação, mas você deve considerar coisas como filas seqüenciais que podem permitir que você acesse um recurso apenas de um único encadeamento.

E há uma estratégia antiga, mas infalível: atribua um "nível" a cada bloqueio, começando no nível 0. Se você usar um bloqueio de nível 0, não será permitido nenhum outro bloqueio. Depois de pegar um bloqueio de nível 1, você pode pegar um bloqueio de nível 0. Depois de pegar um bloqueio de nível 10, você pode travar nos níveis 9 ou inferior, etc.

Se você acha isso impossível, é necessário corrigir o código, pois você encontrará conflitos.

gnasher729
fonte