σμ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 1 · Condições de corrida e o explorador›Tópico 1.2·~1 hAvançado

Um explorador de intercalações: achar bugs enumerando escalonamentos

Posição no roteiroMódulo 1 de 4 · Tópico 2 de 2
1.2 Um explorador de intercalações: achar bugs enumerando escalonamentos
—Nota do lab— / 100XP total—Sequência—

5 min de leitura4 exemplos de código

  1. 01Quantos escalonamentos existem?
  2. 02Threads como geradores
  3. 03Explorar e depois perguntar
  4. 04O que você vai construir

Tentar escalonamentos à mão só acha um bug se você adivinhar o certo. Um explorador executa o programa sob todos os escalonamentos, lista todos os resultados e entrega o escalonamento que o quebrou. Para duas threads que contam os dias de alta de dez retornos do SPY há 210 escalonamentos, e só 10 dão a contagem certa.

01

Quantos escalonamentos existem?

Um escalonamento é uma ordem de turnos, e a thread t toma exatamente lengths[t] deles. Para contar as ordens, colocamos todos os turnos em fila (a soma dos comprimentos), que podem ser arrumados em tantas maneiras quanto o fatorial desse número; depois dividimos pelo fatorial dos turnos de cada thread, porque trocar dois turnos da mesma thread não dá um escalonamento novo. É o coeficiente multinomial. Cresce rápido: duas threads de 2 turnos têm só 6 escalonamentos, três threads de 3 turnos têm 1680 e duas threads de 10 turnos têm 184 756. Por isso ninguém consegue checar um programa concorrente tentando escalonamentos ao acaso e esperando.

main.pyPython
def count_schedules(lengths):    total = factorial(sum(lengths))    for n in lengths:        total //= factorial(n)    return totalprint(count_schedules([2, 2]))print(count_schedules([3, 3, 3]))print(count_schedules([10, 10]))
6
1680
184756
02

Threads como geradores

No laboratório anterior uma thread era uma lista de passos. O código real se escreve melhor em Python comum, então daqui em diante uma thread é um gerador que recebe o dicionário compartilhado e executa yield antes de cada acesso a dados compartilhados. Uma chamada de next() avança a thread até o próximo acesso e não mais, de modo que um turno é exatamente um passo com o qual outra thread pode se intercalar. run_threads prepara cada gerador (executa até o primeiro yield), dá um turno à thread nomeada por cada entrada do escalonamento e, quando o escalonamento acaba, deixa as threads não terminadas terminarem na ordem das threads. all_schedules lista todas as ordens de turnos escolhendo qual thread vai primeiro e recursando no que sobra. A seguir, os seis escalonamentos de duas threads que incrementam uma vez cada.

main.pyPython
def all_schedules(lengths):    if sum(lengths) == 0:        return [[]]    out = []    for tid, n in enumerate(lengths):        if n:            rest = list(lengths)            rest[tid] -= 1            out += [[tid] + tail for tail in all_schedules(rest)]    return outdef run_threads(make_threads, schedule, shared=None):    shared = dict(shared or {})    threads = make_threads(shared)    alive = [True] * len(threads)    def turn(tid):        try:            next(threads[tid])        except StopIteration:            alive[tid] = False    for tid in range(len(threads)):        turn(tid)    for tid in schedule:        if alive[tid]:            turn(tid)    while any(alive):        for tid in range(len(threads)):            if alive[tid]:                turn(tid)    return sharedtwo = lambda shared: [increments(1)(shared), increments(1)(shared)]for schedule in all_schedules([2, 2]):    print(schedule, run_threads(two, schedule))
[0, 0, 1, 1] {'x': 2}
[0, 1, 0, 1] {'x': 1}
[0, 1, 1, 0] {'x': 1}
[1, 0, 0, 1] {'x': 1}
[1, 0, 1, 0] {'x': 1}
[1, 1, 0, 0] {'x': 2}

Dois dos seis escalonamentos terminam em 2, os outros quatro em 1. Os dois bons são aqueles em que uma thread faz seus dois passos antes de a outra começar. Nos outros quatro as duas threads leem antes de qualquer uma escrever, que é exatamente a atualização perdida do laboratório anterior, agora achada sem adivinhar.

03

Explorar e depois perguntar

explore executa o programa sob todos os escalonamentos e conta quantos dão cada estado final. A chave é uma tupla ordenada de pares (nome, valor) para que estados iguais sejam a mesma chave. find_schedule faz a pergunta de um relatório de bug: qual escalonamento quebra o meu invariante? Devolve o primeiro e, como run_threads é determinístico, dá para reproduzi-lo, imprimi-lo ou colá-lo num teste. Se devolve None, o invariante vale sob todos os escalonamentos, o que é uma prova para este programa pequeno, não uma esperança.

main.pyPython
def explore(make_threads, lengths, shared=None):    outcomes = {}    for schedule in all_schedules(lengths):        key = tuple(sorted(run_threads(make_threads, schedule, shared).items()))        outcomes[key] = outcomes.get(key, 0) + 1    return outcomesdef find_schedule(make_threads, lengths, check, shared=None):    for schedule in all_schedules(lengths):        if not check(run_threads(make_threads, schedule, shared)):            return schedule    return Noneprint(explore(two, [2, 2]))print(find_schedule(two, [2, 2], lambda shared: shared["x"] == 2))
{(('x', 2),): 2, (('x', 1),): 4}
[0, 1, 0, 1]
O espaço cresce fatorialmente

A enumeração por força bruta funciona com poucos turnos por thread e depois explode. Exploradores reais podam: lembram dos estados já vistos e pulam escalonamentos que só reordenam passos que não podem afetar um ao outro. A ideia continua a mesma, então o que você constrói aqui é o modelo mental certo.

main.pyPython
returns = [float(r) for r in load_bars()["close"].pct_change().dropna()][:10]ups = [sum(1 for r in returns[start::2] if r > 0) for start in (0, 1)]lengths = [2 * k for k in ups]many = lambda shared: [increments(k)(shared) for k in ups]print(ups, count_schedules(lengths))print({dict(key)["x"]: n for key, n in explore(many, lengths).items()})
[2, 3] 210
{5: 10, 4: 70, 3: 104, 2: 26}
O que dizem os dados reaisos primeiros 10 retornos do SPY, duas threads contando dias de alta
Escalonamentos210para 2 e 3 dias de alta
Resposta certa10escalonamentos terminam em x = 5
Perdem atualizações200de 210 escalonamentos
Pior caso2x no fim, de 5
Escalonamentos pelo valor final de x
26
104
70
10

A resposta certa, x = 5, aparece em só 10 dos 210 escalonamentos, cerca de 5%. Um teste que executa o programa uma vez, ou cem vezes numa máquina real onde as threads raramente se sobrepõem exatamente assim, passaria quase sempre e ainda assim entregaria um bug. O explorador não pode perdê-lo, e find_schedule entrega o primeiro culpado: [0, 0, 0, 1, 0, 1, 1, 1, 1, 1].

DefiniçãoInvariante

Uma afirmação sobre os dados que deve valer seja qual for o escalonamento. Um programa concorrente está correto quando seu invariante vale sob todos os escalonamentos, e explorar todos os escalonamentos é a forma de verificá-lo num caso pequeno.

04

O que você vai construir

O laboratório segue a aula: o número de escalonamentos, a lista de todos, a máquina que executa threads geradoras sob um escalonamento, o explorador que conta resultados, a busca do primeiro escalonamento que quebra um invariante e o estudo de duas threads contando dias de alta do SPY.

  • Função · perguntaBaseia-se em
  • count_schedules(lengths)Quantos escalonamentos o programa tem?Aritmética simples
  • all_schedules(lengths)Quais são, em ordem?Aritmética simples
  • run_threads(make_threads, schedule, shared)O que um escalonamento deixa?Aritmética simples
  • explore(make_threads, lengths, shared)Quais resultados existem e quantas vezes?all_schedulesrun_threads
  • find_schedule(make_threads, lengths, check)Qual escalonamento quebra o invariante primeiro?all_schedulesrun_threads
  • study(bars)Quantos escalonamentos da contagem do SPY dão errado?count_schedulesexplorefind_schedule

Pontos-chave

  • Conte primeiro os escalonamentos. O coeficiente multinomial cresce fatorialmente: 6 escalonamentos para duas threads de 2 turnos, 184 756 para duas de 10.
  • Um yield marca cada acesso compartilhado. Escrever as threads como geradores faz de um turno um acesso, então o explorador pode tentar todas as intercalações de código Python comum.
  • Um invariante mais todos os escalonamentos é uma prova. Só 10 de 210 escalonamentos deram a contagem certa; find_schedule devolve o primeiro que não deu, pronto para reproduzir.
Verificação rápida
Duas threads tomam 3 turnos cada uma. Quantos escalonamentos diferentes elas têm?
Escolha uma resposta.

Próximo tópico: o explorador mostra que o bug é real; agora o consertamos. Atômicas e leitura-modificação-escrita acrescenta as operações que um processador faz num passo indivisível, como fetch-and-add e compare-and-swap, e você confere com o explorador que o contador está certo sob todos os escalonamentos.

Lab: o que é avaliado

6 tarefas · 100 pts
  • Quantos escalonamentos10 pts
  • Cada escalonamento15 pts
  • Executar as threads25 pts
  • Contar cada resultado20 pts
  • O primeiro escalonamento ruim10 pts
  • O estudo com a tabela do SPY20 pts
Explorador · lab-lf-02
▾ lab-lf-02
▾ data
…
bash · no navegador
learner@quantcraft:~/lab-lf-02$
Python ainda não carregadoPythonLn 1, Col 1Spaces: 4UTF-8

Redefinir este tópico?

Tudo em lab-lf-02 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