Depurar uma corrida
O defeito que some quando você olha — e as quatro técnicas que funcionam mesmo assim.
Uma condição de corrida tem a pior propriedade possível para depuração: ela some quando se olha. Acrescentar um out muda o tempo, o depurador serializa as threads, e o defeito não reproduz — o que leva à conclusão errada de que ele foi corrigido.
1. Aumentar a janela, de propósito#
Se você suspeita de um ler-modificar-escrever, alargue o intervalo entre ler e escrever. Um defeito que acontece em 1 de 10 mil passa a acontecer em 9 de 10:
dataforge
adopt Arcane.Concurrent as C
estado := {"n": 0}
action somar_com_janela():
cycle i from 1 to 50:
lido := estado["n"]
sleep(1) // a janela, alargada de propósito
estado["n"] := lido + 1
parallel:
somar_com_janela()
somar_com_janela()
// Com a janela aberta, a perda aparece quase sempre.
out $"esperado 100, obtido {estado['n']}"
assert estado["n"] <= 100Isto é uma técnica de investigação, e não um teste: o teste que fica é o que usa a trava, e ele precisa passar sempre.
2. Contar, em vez de olhar#
dataforge
adopt Arcane.Concurrent as C
// Um contador atômico não perde, e por isso serve de RÉGUA: ele diz
// quantas vezes o trecho rodou de verdade.
feitas := C.contador(0)
estado := {"n": 0}
action trabalho():
cycle i from 1 to 500:
feitas.somar(1)
estado["n"] := estado["n"] + 1
parallel:
trabalho()
trabalho()
out $"rodou {feitas.valor()} vezes, e o estado marcou {estado['n']}"
assert feitas.valor() is 1000A régua é o que transforma "acho que perdeu atualização" em "rodou mil vezes e o estado marcou 987" — e a segunda frase aponta para a linha.
3. Repetir muitas vezes, e olhar a distribuição#
dataforge
adopt Arcane.Concurrent as C
action rodada():
estado := {"n": 0}
action somar():
cycle i from 1 to 300:
estado["n"] := estado["n"] + 1
parallel:
somar()
somar()
yield estado["n"]
resultados := [rodada() cycle i in range(0, 5)]
distintos := len(set(resultados))
out $"5 rodadas, {distintos} resultado(s) distinto(s): {sorted(resultados)}"
// Um resultado que MUDA entre rodadas idênticas é a assinatura de
// uma corrida — e um que não muda não prova ausência.
assert distintos >= 14. O depurador, com a ressalva#
| Ferramenta | Serve para | Não serve para |
|---|---|---|
dataforge debug | ver o estado de uma thread parada | reproduzir a corrida — ele a serializa |
vigia (w saldo) | descobrir quem mudou o valor | o mesmo: a parada muda o tempo |
C.contador | contar sem perder | dizer onde |
um out com id da thread | ver a ordem que aconteceu | casos raros: o out tem trava por dentro |
dataforge
adopt Arcane.Concurrent as C
// O id da thread no registro é o que deixa reconstruir a ordem.
linhas := []
action trabalho(nome):
cycle i from 1 to 3:
linhas.append($"{nome}:{i}")
parallel:
trabalho("a")
trabalho("b")
assert len(linhas) is 6
out linhasOnde procurar primeiro#
- `v["n"] := v["n"] + 1` em qualquer lugar alcançado por duas threads — é o caso nº 1, e o
checkavisa. - Uma rota do Kiln que escreve em estado de fora: a concorrência ali é invisível.
- `remove`, `pop`, `insert`, `sort` — eles leem para decidir o que escrever.
- Duas travas em ordens diferentes — não é perda de atualização, é impasse, e o sintoma é um travamento.
- Um recurso compartilhado sem dono claro — se você não consegue dizer quem é o dono, provavelmente não há um.