Pular para o conteúdo

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"] <= 100

Isto é 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 1000

A 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 >= 1

4. O depurador, com a ressalva#

FerramentaServe paraNão serve para
dataforge debugver o estado de uma thread paradareproduzir a corrida — ele a serializa
vigia (w saldo)descobrir quem mudou o valoro mesmo: a parada muda o tempo
C.contadorcontar sem perderdizer onde
um out com id da threadver a ordem que aconteceucasos 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 linhas

Onde procurar primeiro#

  • `v["n"] := v["n"] + 1` em qualquer lugar alcançado por duas threads — é o caso nº 1, e o check avisa.
  • 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.