Atômicos e estruturas sem trava
CAS, fetch-add, fila e pilha sem trava, ring buffer, executor e promessa — e o que o GIL garante de verdade.
O Contador resolve contar. O que faltava era a peça geral: um valor que troca só se ainda for o que você leu — o compare-and-swap. É com ele que se escreve um contador sem trava, uma pilha sem trava e metade de uma STM.
CAS: a troca condicional#
adopt Arcane.Concurrent as C
a := C.atomico(10)
assert a.pegar() is 10
assert a.comparar_e_trocar(10, 20) // ainda era 10: trocou
assert a.pegar() is 20
assert not a.comparar_e_trocar(10, 30) // já não é 10: não trocou
assert a.trocar(99) is 20 // devolve o ANTERIOR
assert a.pegar() is 99O laço clássico — leia, calcule, troque se ninguém mexeu — é a forma de escrever qualquer atualização sem trava:
adopt Arcane.Concurrent as C
a := C.atomico(0)
action incrementar_sem_trava():
persist yes:
atual := a.pegar()
given a.comparar_e_trocar(atual, atual + 1):
halt
action trabalhar(i):
cycle _ in range(0, 200):
incrementar_sem_trava()
C.para_cada(trabalhar, [i cycle i in range(1, 11)])
assert a.pegar() is 2000| Símbolo | O que faz |
|---|---|
a.pegar() · a.definir(x) | leitura e escrita indivisíveis |
a.trocar(x) | escreve e devolve o anterior |
a.comparar_e_trocar(esperado, novo) | troca só se o valor ainda for o esperado; devolve se trocou |
a.somar(n) | soma e devolve o novo (fetch-add com a ordem certa) |
a.pegar_e_somar(n) | soma e devolve o anterior |
a.atualizar(f) | aplica f, repetindo enquanto alguém trocar |
Fila, pilha e anel#
adopt Arcane.Concurrent as C
fila := C.fila_sem_trava()
fila.por(1)
fila.por(2)
assert fila.tirar() is 1 and fila.tamanho() is 1
pilha := C.pilha_sem_trava()
pilha.por(1)
pilha.por(2)
assert pilha.tirar() is 2 // do topo
assert pilha.tirar() is 1
assert pilha.tirar() is void // vazia: void, e não erro
anel := C.anel(3)
cycle i from 1 to 5:
anel.por(i)
assert anel.tudo() is [3, 4, 5] // o mais velho sai quando enche
assert anel.cheio()O anel é a estrutura de uma janela de métricas, de um log em memória e de um produtor rápido com consumidor lento — onde perder o mais velho é melhor que parar o produtor.
O que o GIL garante — e o que não#
| Item da literatura | Aqui |
|---|---|
| compare-and-swap | atomico.comparar_e_trocar — existe, e é a base do resto |
| fetch-add, atomic load/store | somar, pegar_e_somar, pegar, definir |
| acquire, release, relaxed, seq-cst, fences | não se aplicam: não há reordenação observável a ordenar — o GIL já dá consistência sequencial entre operações Python |
| wait-free | não existe como garantia: o laço de CAS é lock-free, não wait-free — uma thread azarada pode repetir |
| ABA problem | acontece, e a saída é a mesma: guarde um selo junto do valor ((valor, versão)) em vez do valor cru |
| hazard pointers, epoch reclamation | não se aplicam: o coletor cuida da liberação, que é o problema que essas técnicas resolvem |
Executor e promessa#
C.rodar abre uma thread por chamada; num servidor isso acontece por pedido. O executor paga a partida uma vez. A promessa é o resultado que ainda não existe e que alguém — um evento, uma resposta de rede — vai cumprir.
adopt Arcane.Concurrent as C
executor := C.executor(4)
action dobro(x):
yield x * 2
tarefas := [executor.submeter(dobro, i) cycle i in range(1, 4)]
assert [C.esperar(t) cycle t in tarefas] is [2, 4, 6]
assert executor.mapear(dobro, [10, 20]) is [20, 40]
executor.fechar()
assert not executor.aberto()
p := C.promessa()
action cumprir(i):
p.cumprir("pronto")
C.rodar(cumprir, 1)
assert p.esperar(3000) is "pronto" Uma promessa que falha leva a falha a quem espera — e não um valor vazio:
adopt Arcane.Concurrent as C
p := C.promessa()
p.falhar("deu ruim")
monitor:
p.esperar(1000)
assert no
handle Error as e:
assert "deu ruim" in e.message