σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsRoteiros
Ctrl K
AO
+1,2 mil alunos
EntrarParticipar ↗
RoadmapsRoteiros
AO
+1,2 mil alunos
Entrar
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsRoteiros
Ctrl K
AO
+1,2 mil alunos
EntrarParticipar ↗
RoadmapsRoteiros
AO
+1,2 mil alunos
Entrar
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Roteiro de Lock-free›Módulo 2 · Atômicas, ordem e garantias›Tópico 2.1·~55 minAvançado

Atômicas e leitura-modificação-escrita

Posição no roteiroMódulo 2 de 4 · Tópico 1 de 3
2.1 Atômicas e leitura-modificação-escrita
—Nota do lab— / 100XP total—Sequência—

5 min de leitura4 exemplos de código

  1. 01Um turno, sem intervalo
  2. 02Compara-e-troca e o laço de nova tentativa
  3. 03Explorar quando o número de turnos varia
  4. 04O que você vai construir

Uma atualização perdida acontece porque uma leitura e uma escrita são dois turnos com um intervalo entre eles. Uma operação atômica fecha o intervalo: a leitura, a mudança e a escrita acontecem num passo indivisível. Com ela o contador está certo sob todos os escalonamentos: 4 escalonamentos em vez de 28, e nenhum errado.

01

Um turno, sem intervalo

Na nossa máquina um turno vai de um yield ao seguinte, então nada escrito entre dois yield pode ser intercalado. Uma função que não contém nenhum yield se comporta, portanto, como uma operação atômica. A mais comum é fetch-and-add: lê o valor antigo, escreve old + n e devolve old, tudo dentro de um turno. Uma thread contadora que a chama tem um só yield para todo o incremento em vez de dois, então as duas threads não podem mais ler o mesmo valor antigo.

main.pyPython
def fetch_add(shared, name, n):    old = shared.get(name, 0)    shared[name] = old + n    return olddef atomic_increments(k):    def thread(shared):        for _ in range(k):            yield            fetch_add(shared, "x", 1)    return threadtwo = lambda shared: [atomic_increments(1)(shared), atomic_increments(1)(shared)]for prefix in ([0, 1], [1, 0]):    print(prefix, run_prefix(two, prefix))
[0, 1] ({'x': 2}, [])
[1, 0] ({'x': 2}, [])

As duas ordens terminam em {'x': 2} e nenhuma thread fica viva. Com um turno por incremento o programa só tem os dois escalonamentos [0, 1] e [1, 0] (4 no estudo, para 1 e 3 incrementos), e nenhum pode perder uma atualização.

02

Compara-e-troca e o laço de nova tentativa

Fetch-and-add só soma. Para todo o resto é preciso a ferramenta geral, a compara-e-troca (CAS). O padrão é um laço: ler o valor antigo, calcular o novo a partir dele e pedir ao CAS que o instale só se ninguém mudou o valor nesse meio-tempo. Se o CAS falha, alguém chegou antes, então lê-se de novo e tenta-se outra vez. A seguir, dois contadores CAS: quando a thread 1 lê antes de a thread 0 escrever, seu CAS falha uma vez, conta uma nova tentativa e a segunda tentativa tem sucesso. A atualização nunca se perde, porque uma escrita obsoleta é recusada.

main.pyPython
def compare_and_swap(shared, name, expected, new):    if shared.get(name, 0) != expected:        return False    shared[name] = new    return Truedef cas_increments(k):    def thread(shared):        for _ in range(k):            while True:                yield                old = shared.get("x", 0)                yield                if compare_and_swap(shared, "x", old, old + 1):                    break                fetch_add(shared, "retries", 1)    return threadcas = lambda shared: [cas_increments(1)(shared), cas_increments(1)(shared)]print(run_prefix(cas, [0, 1, 0, 1]))print(run_prefix(cas, [0, 1, 0, 1, 1, 1]))
({'x': 1, 'retries': 1}, [1])
({'x': 2, 'retries': 1}, [])

Depois de [0, 1, 0, 1] o dicionário compartilhado é {'x': 1, 'retries': 1} e a thread 1 ainda está viva: leu 0, mas a thread 0 já tinha guardado 1, então sua compara-e-troca foi recusada. Dando à thread 1 seus dois turnos seguintes (ler de novo, trocar de novo) termina em x = 2. Note o preço: o perdedor fez o trabalho duas vezes. Fetch-and-add nunca tenta de novo; o CAS tenta tantas vezes quanto as threads colidem.

03

Explorar quando o número de turnos varia

O explorador do tópico anterior supunha que cada thread toma um número fixo de turnos, lengths. Um laço de nova tentativa quebra isso: uma thread que perde uma corrida toma turnos extras, e quantos depende do escalonamento. A solução é percorrer os escalonamentos como uma árvore. A partir de um prefixo de turnos, run_prefix refaz o programa e diz quais threads ainda estão vivas; se nenhuma está, o escalonamento está completo e seu estado final é contado, e se há, ramifica-se em cada uma. Esta busca em profundidade visita cada escalonamento completo exatamente uma vez. values_of lê depois um nome compartilhado de todos os resultados.

main.pyPython
def explore_all(make_threads, shared=None):    outcomes = {}    def visit(prefix):        final, alive = run_prefix(make_threads, prefix, shared)        if not alive:            key = tuple(sorted(final.items()))            outcomes[key] = outcomes.get(key, 0) + 1            return        for tid in alive:            visit(prefix + [tid])    visit([])    return outcomesdef values_of(outcomes, name):    return sorted({dict(key).get(name, 0) for key in outcomes})print(explore_all(cas))racy = lambda shared: [increments(1)(shared), increments(1)(shared)]print(values_of(explore_all(racy), "x"), values_of(explore_all(cas), "x"))
{(('x', 2),): 2, (('retries', 1), ('x', 2)): 4}
[1, 2] [2]
Atômico não é de graça

Nesta máquina cada operação é vista por todas as threads ao mesmo tempo e em ordem. Um processador real não promete isso para leituras e escritas comuns, e mesmo uma operação atômica faz os núcleos disputarem a linha de cache. O próximo tópico remove a primeira suposição.

main.pyPython
returns = [float(r) for r in load_bars()["close"].pct_change().dropna()][:6]ups = [sum(1 for r in returns[start::2] if r > 0) for start in (0, 1)]for name, factory in (("racy", increments), ("atomic", atomic_increments), ("cas", cas_increments)):    outcomes = explore_all(lambda shared, f=factory: [f(k)(shared) for k in ups])    print(name, sum(outcomes.values()), "schedules, x in", values_of(outcomes, "x"))
racy 28 schedules, x in [1, 2, 3, 4]
atomic 4 schedules, x in [4]
cas 144 schedules, x in [4]
O que dizem os dados reaisos primeiros 6 retornos do SPY, uma thread com 1 dia de alta e outra com 3
Contador com corrida24dos 28 escalonamentos estão errados
fetch-and-add0dos 4 escalonamentos estão errados
Compara-e-troca0dos 144 escalonamentos estão errados
Pior número de novas tentativas3novas tentativas do contador CAS
Escalonamentos por contador
fetch-and-add4
Com corrida28
Compara-e-troca144

As duas correções estão certas sob todos os escalonamentos e diferem no custo. Fetch-and-add faz o trabalho em um turno por incremento: 4 escalonamentos. O contador CAS tem 144 escalonamentos, porque uma colisão acrescenta turnos, e no pior deles uma thread tenta de novo 3 vezes. É o compromisso do código sem travas: ninguém espera uma trava, mas o vencedor de cada corrida obriga o perdedor a repetir seu trabalho.

DefiniçãoLeitura-modificação-escrita

Uma operação que lê um valor compartilhado, calcula um novo a partir dele e o escreve de volta num passo indivisível. Fetch-and-add e compara-e-troca são as duas que você mais usará; toda estrutura sem travas é construída com elas.

04

O que você vai construir

O laboratório segue a aula: fetch-and-add, compara-e-troca, uma thread contadora construída sobre cada um, o explorador que lida com threads de número de turnos variável, um auxiliar que lê os valores de um nome em todos os resultados e o estudo de três contadores nos dias de alta do SPY.

  • Função · perguntaBaseia-se em
  • fetch_add(shared, name, n)Somar n, e o que havia antes?Aritmética simples
  • compare_and_swap(shared, name, expected, new)O valor ainda era o que li?Aritmética simples
  • atomic_increments(k)Um contador sem intervalo?fetch_add
  • cas_increments(k)Um contador que tenta de novo em vez de perder?compare_and_swapfetch_add
  • explore_all(make_threads, shared)Todos os resultados, seja qual for o número de turnos?run_prefix
  • values_of(outcomes, name)Que valores esse nome assume?Aritmética simples
  • study(bars)Como os três contadores se comparam no SPY?explore_allvalues_of

Pontos-chave

  • Atômico significa nenhum yield dentro. Uma leitura-modificação-escrita em um turno não pode ser intercalada, então fetch-and-add deixa 4 escalonamentos, todos certos.
  • O CAS transforma um conflito em nova tentativa. Uma escrita obsoleta é recusada em vez de perdida: 144 escalonamentos, nenhum errado, no máximo 3 novas tentativas.
  • Explore como árvore quando os turnos variam. Refaça cada prefixo e ramifique nas threads ainda vivas; cada escalonamento completo é visitado uma vez.
Verificação rápida
A thread 0 e a thread 1 executam cada uma um incremento com compara-e-troca. Ambas leem x = 0 antes de qualquer uma trocar. O que acontece depois?
Escolha uma resposta.

Próximo tópico: supusemos que cada thread vê cada escrita imediatamente e em ordem. Um processador real não promete isso. O modelo de memória: acquire e release acrescenta um buffer de escrita à máquina, mostra um leitor vendo uma flag antes dos dados que ela protege e o corrige com a ordem que menos custa.

Lab: o que é avaliado

7 tarefas · 100 pts
  • Fetch-and-add5 pts
  • Compara-e-troca10 pts
  • Um contador com fetch-and-add10 pts
  • Um contador com laço de nova tentativa20 pts
  • Explorar todos os escalonamentos20 pts
  • Os valores de um nome5 pts
  • O estudo com a tabela do SPY30 pts
Explorador · lab-lf-03
▾ lab-lf-03
▾ data
…
bash · no navegador
learner@quantcraft:~/lab-lf-03$
Python ainda não carregadoPythonLn 1, Col 1Spaces: 4UTF-8

Redefinir este tópico?

Tudo em lab-lf-03 volta aos arquivos iniciais e o terminal é limpo. Sua solução atual será perdida — isso não pode ser desfeito.

Será redefinido
  • main.py → código inicial
  • Histórico do terminal e saída dos testes
  • Alterações não enviadas
Será mantido
  • Melhor nota do lab
  • Seu XP e sua sequência
  • Progresso da aula e anotações