39 · Concorrência avançada
1 exercícios: .
python3 exercicios/run_all.py 39Os exercícios#
| # | Título | Enunciado |
|---|---|---|
| 257 | Memoria transacional, CAS e estruturas sem trava |
257 · Memoria transacional, CAS e estruturas sem trava#
// O problema esta medido neste repositorio: duas threads somando na
// mesma variavel entregaram 40.425 de 80.000, em silencio.
//
// O mutex resolve UMA secao critica. O que ele nao resolve e COMPOR:
// transferir de uma conta para outra sao duas escritas que precisam
// acontecer juntas — e com mutex isso vira ordem de aquisicao, que
// ninguem consegue verificar.
adopt Arcane.Stm as T
adopt Arcane.Concurrent as C
adopt Arcane.Collections as Col
// ── o incremento que nao se perde ──
total := T.variavel(0)
action somar_muitas(i):
cycle _ in range(0, 200):
T.atomicamente(lambda => T.escrever(total, T.ler(total) + 1))
C.para_cada(somar_muitas, [i cycle i in range(1, 41)])
assert T.valor(total) is 8000 // 40 threads x 200, nada perdido
// ── atomicidade: metade escrita nao existe ──
a := T.variavel(10)
b := T.variavel(10)
action quebrar_no_meio():
T.escrever(a, 999)
trigger "no meio"
monitor:
T.atomicamente(quebrar_no_meio)
assert no
handle Error as e:
assert e.message is "no meio"
assert T.valor(a) is 10 and T.valor(b) is 10
// ── isolamento: a transacao le a propria escrita ──
x := T.variavel(1)
action ler_e_escrever():
primeiro := T.ler(x)
T.escrever(x, 5)
yield [primeiro, T.ler(x)]
assert T.atomicamente(ler_e_escrever) is [1, 5]
assert T.valor(x) is 5
// ── composicao: aninhar e achatar ──
p := T.variavel(1)
q := T.variavel(1)
action dobrar_p():
T.escrever(p, T.ler(p) * 2)
action dobrar_q():
T.escrever(q, T.ler(q) * 2)
action as_duas():
dobrar_p() // ja e transacional
dobrar_q() // esta tambem
T.atomicamente(as_duas) // e as duas viram UMA
assert T.valor(p) is 2 and T.valor(q) is 2
// ── esperar sem girar ──
fila := T.variavel([])
reserva := T.variavel(["de reserva"])
action da_fila():
itens := T.ler(fila)
given len(itens) is 0:
T.retentar() // vazia: dorme ate mudar
yield itens[0]
action da_reserva():
yield T.ler(reserva)[0]
assert T.atomicamente(lambda => T.ou_entao(da_fila, da_reserva)) is "de reserva"
// ler fora de transacao e recusado: nao teria garantia nenhuma
monitor:
T.escrever(x, 9)
assert no
handle RuntimeError as e:
assert "transa" in e.message
// ── CAS: a troca condicional ──
atomo := C.atomico(10)
assert atomo.comparar_e_trocar(10, 20) // ainda era 10
assert not atomo.comparar_e_trocar(10, 30) // ja nao e
assert atomo.trocar(99) is 20 // devolve o ANTERIOR
assert atomo.pegar() is 99
// o contador sem trava, escrito com CAS
contador := C.atomico(0)
action incrementar():
persist yes:
atual := contador.pegar()
given contador.comparar_e_trocar(atual, atual + 1):
halt
action trabalhar(i):
cycle _ in range(0, 200):
incrementar()
C.para_cada(trabalhar, [i cycle i in range(1, 11)])
assert contador.pegar() is 2000
// ── estruturas sem trava ──
f := C.fila_sem_trava()
action produzir(i):
cycle j in range(0, 100):
f.por(i * 1000 + j)
C.para_cada(produzir, [i cycle i in range(1, 9)])
assert f.tamanho() is 800
vistos := []
persist f.tamanho() bigger 0:
item := f.tirar()
given item isnt void:
vistos.append(item)
assert len(vistos) is 800
assert len(Col.set(vistos)) is 800 // nada duplicado
pilha := C.pilha_sem_trava()
pilha.por(1)
pilha.por(2)
assert pilha.tirar() is 2 and pilha.tirar() is 1
assert pilha.tirar() is void // vazia: void, e nao 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
// ── executor e promessa ──
executor := C.executor(4)
action dobro(n):
yield n * 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()
promessa := C.promessa()
action cumprir_depois(i):
promessa.cumprir("pronto")
C.rodar(cumprir_depois, 1)
assert promessa.esperar(3000) is "pronto"
// uma promessa que falha leva a falha a quem espera
ruim := C.promessa()
ruim.falhar("deu ruim")
monitor:
ruim.esperar(1000)
assert no
handle Error as e:
assert "deu ruim" in e.message
out "257 ok"O problema, medido#
Duas threads somando na mesma variável entregaram 40.425 de 80.000 neste repositório — em silêncio. O mutex resolve uma seção crítica. O que ele não resolve é compor: transferir de uma conta para outra são duas escritas que precisam acontecer juntas, e com mutex isso vira ordem de aquisição — uma regra que ninguém consegue verificar, e cuja violação é impasse.
A transação#
T.atomicamente(acao) roda a ação num rascunho: tudo o que ela lê guarda a versão, e tudo o que ela escreve fica de lado. No fim, sob uma trava curta, ela confere se alguma variável lida mudou. Se mudou, descarta e tenta de novo; se não, publica tudo de uma vez.
Quatro consequências:
1. atomicidade — um erro no meio não deixa metade escrita, e o erro sobe (engoli-lo seria o oposto de atomicidade); 2. isolamento — a transação lê a própria escrita, não a dos outros; 3. composição — aninhar é achatar: duas ações transacionais dentro de uma terceira são uma transação; 4. conflito custa repetição, e não dado errado. As estatísticas dizem quanto.
Esperar sem girar#
T.retentar() diz "não dá para seguir com o que existe agora". A transação é abandonada e dorme até alguma variável que ela leu mudar. T.ou_entao(a, b) tenta a segunda quando a primeira pede para esperar — é a composição de duas operações bloqueantes, que um mutex não tem.
Nunca faça E/S dentro de uma transação#
Ela pode ser repetida, e o que já saiu não volta: um out, um IO.write ou um Http.post lá dentro aconteceria duas vezes. Junte o resultado dentro e faça a E/S depois.
CAS: a peça de baixo#
comparar_e_trocar(esperado, novo) troca só se o valor ainda for o que você leu. O laço clássico — leia, calcule, troque se ninguém mexeu — é como se escreve qualquer atualização sem trava.
O que "sem trava" quer dizer aqui#
append e popleft de um deque acontecem inteiros em C: não há janela entre ler e escrever. Isso está medido — quatro threads com 5 mil append cada entregaram 20.000 de 20.000.
O que não é indivisível é qualquer sequência escrita em DataForge: v["n"] := v["n"] + 1 perde atualização. Ali a resposta é atomico, mutex ou transação.
E lock-free não é wait-free: o laço de CAS repete quando há disputa, e uma thread azarada pode repetir muitas vezes.
Rode um isolado com dataforge run exercicios/39-concorrencia-avancada/257_stm_e_atomicos.df.