Pular para o conteúdo

Condição de corrida

Ler, somar e escrever não é uma operação — e duas threads fazendo isso perdem metade das somas, caladas.

v["n"] := v["n"] + 1 parece uma operação e são três: ler, somar, escrever. Entre o ler e o escrever de uma thread, outra thread lê o mesmo valor velho — e as duas escrevem o mesmo resultado. Uma soma some, sem erro nenhum.

Operação, 4 threadsEsperadoMedido
v["n"] := v["n"] + 1 sem trava40.00033.740
xs.append(i) sem trava20.00020.000 — o GIL protege a operação inteira
com P.mutex ou P.ator20.00020.000

O append sozinho não perde porque é uma operação do interpretador, protegida pelo GIL. O que perde é o que lê para decidir o que escrever: += 1, pop, remove, insert, sort. É essa a lista que o check usa.

O check avisa#

dataforge
adopt Arcane.Process as Proc
adopt Arcane.IO as IO
adopt Arcane.OS as OS

pasta := $"{OS.temp_dir()}/df-corrida-{randint(100000, 999999)}"
IO.mkdir(pasta)
arquivo := $"{pasta}/corrida.df"
IO.write(arquivo, "total := {\"n\": 0}\nparallel:\n    total[\"n\"] := total[\"n\"] + 1\n    total[\"n\"] := total[\"n\"] + 1\n")

// o 'check' do mesmo Python que roda este programa — e não um do PATH
r := Proc.run([OS.executable(), "-m", "dataforge", "check", arquivo])
saida := r["stdout"] + r["stderr"]
assert saida.contains("corrida.df:3") and saida.contains("'total'")   // o aviso, na linha certa
IO.remove_tree(pasta)

dataforge check num arquivo assim avisa escrita-concorrente: um thread, parallel ou `route` escrevendo num nome que vem de fora. A rota é o caso que mais importa, porque o Kiln atende cada pedido numa thread, e ali a concorrência é invisível — quem escreve a rota não vê thread nenhuma. Medido: seis pedidos simultâneos numa rota que lê, espera e escreve entregaram 1 de 6.