σμ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ᵀμ = μ*∫Σ∂∞λ
σμ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 AcademyRoteiros
Ctrl K
AO
+1,2 mil alunos
EntrarParticipar ↗
Roteiros
AO
+1,2 mil alunos
Entrar
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Roteiro de Python›Módulo 10 · Engenharia Python de alto desempenho›Tópico 10.9·~55 minAvançado

Threads e o GIL

Posição no roteiroMódulo 10 de 11 · Tópico 9 de 13
10.9 Threads e o GIL
—Nota do lab— / 100XP total—Sequência—

Threads: concorrência dentro de um processo

Uma thread é uma linha de execução independente dentro de um mesmo programa, que compartilha a memória com as outras threads. Um programa de trading que precisa buscar cotações de dez mercados, gravar um log e manter a interface responsiva usa threads para que esperar uma não pare as demais. O módulo threading do Python e, de forma mais cômoda, o concurrent.futures.ThreadPoolExecutor as iniciam e gerenciam. Como as threads compartilham variáveis, comunicam-se rápido e é perigoso errar.

# Ten slow "network calls" in a thread pool wait at the same time, so the total is about one call, not ten
import time
from concurrent.futures import ThreadPoolExecutor

def fetch_quote(symbol):
    time.sleep(0.1)                                  # stands in for waiting on a venue
    return symbol, len(symbol) * 10.0

symbols = ["AAPL", "MSFT", "NVDA", "TSLA", "AMZN", "META", "GOOG", "NFLX", "ORCL", "INTC"]

start = time.perf_counter()
with ThreadPoolExecutor(max_workers=10) as pool:
    quotes = list(pool.map(fetch_quote, symbols))
elapsed = time.perf_counter() - start

print(quotes[0], len(quotes))                        # ('AAPL', 40.0) 10
print(elapsed < 0.5)                                 # True  about 0.1 s in total, not 10 x 0.1 s

O GIL: threads que esperam e threads que calculam

O CPython tem um bloqueio global do interpretador (GIL): só uma thread executa bytecode Python em cada instante. Uma thread o cede enquanto espera a rede, um arquivo ou time.sleep, então as threads sobrepõem muito bem as esperas: dez threads buscando cotações terminam em cerca do tempo de uma. Mas para código Python limitado pela CPU (um laço Python puro fazendo aritmética) as threads se revezam, então quatro threads não são mais rápidas que uma e muitas vezes um pouco mais lentas. Código que roda fora do Python, como o NumPy ou uma extensão C que libera o GIL, pode rodar em paralelo em threads. Para cálculo pesado em Python puro, use processos separados (próximo tópico) ou a versão sem GIL (free-threaded) do Python 3.13+, onde disponível.

# Splitting a list into chunks for workers: sizes differ by at most one, the first chunks get the extra items
def split_chunks(items, workers):
    size, extra = divmod(len(items), workers)
    chunks, start = [], 0
    for index in range(workers):
        stop = start + size + (1 if index < extra else 0)
        chunks.append(list(items[start:stop]))
        start = stop
    return chunks

chunks = split_chunks(list(range(7)), 3)
print(chunks)                                        # [[0, 1, 2], [3, 4], [5, 6]]
print([sum(chunk) for chunk in chunks])              # [3, 7, 11]   one partial sum per worker
print(sum(sum(chunk) for chunk in chunks))           # 21

Dados compartilhados, condições de corrida e locks

Threads que atualizam a mesma variável podem corrompê-la, porque total += amount são três passos (ler, somar, escrever) e pode haver troca de thread entre eles: duas threads leem 100, somam 5, escrevem 105, e uma atualização se perde. É uma condição de corrida, que aparece raramente e de forma imprevisível, o que a torna o pior tipo de bug. O remédio é um lock: with lock: deixa uma thread por vez executar o bloco, e tanto a leitura quanto a escrita devem estar dentro. Melhor ainda, evite compartilhar: dê a cada trabalhador seu pedaço e seu resultado e combine os resultados no fim, ou passe trabalho e resultados por uma queue.Queue, que já é segura para threads.

# A lock makes the read-modify-write a single step; the total comes out exact
import threading

totals = {"AAPL": 0}
lock = threading.Lock()

def safe_add(shared, key, amount, lock):
    with lock:
        shared[key] = shared.get(key, 0) + amount
        return shared[key]

def worker():
    for _ in range(10_000):
        safe_add(totals, "AAPL", 1, lock)

threads = [threading.Thread(target=worker) for _ in range(4)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print(totals["AAPL"])                                # 40000  nothing lost: four threads x 10,000 additions
# A back-of-envelope estimate before writing any threaded code
import math

def estimate_time(n_tasks, seconds, workers, kind):
    if kind in ("io", "processes"):
        return math.ceil(n_tasks / workers) * seconds
    return n_tasks * seconds                         # "cpu_threads": the GIL makes the threads take turns

print(estimate_time(40, 0.5, 8, "io"))               # 2.5    forty slow requests, eight at a time
print(estimate_time(40, 0.5, 8, "cpu_threads"))      # 20.0   forty CPU-bound tasks in threads: no gain
print(estimate_time(40, 0.5, 8, "processes"))        # 2.5    separate processes use separate cores
Threads são a ferramenta errada mais vezes do que você pensaThreads servem para E/S: muitas esperas lentas e independentes. Para muitas conexões em um processo, o asyncio (visto antes neste roteiro) costuma escalar melhor e não tem corridas entre await. Para aritmética em arrays, vetorize com NumPy. Para Python limitado pela CPU, use processos. Se compartilhar dados entre threads, mantenha curta a seção travada, nunca chame código lento segurando um lock e pegue vários locks sempre na mesma ordem, ou duas threads podem esperar uma pela outra para sempre (um deadlock).
  • Threads compartilham memória e sobrepõem bem as esperas (rede, arquivos); um ThreadPoolExecutor pode buscar de muitos mercados ao mesmo tempo.
  • O GIL deixa só uma thread executar bytecode Python por vez: sem ganho para Python puro limitado pela CPU, mas NumPy e extensões C podem liberá-lo.
  • Proteja atualizações compartilhadas com with lock: em volta da leitura e da escrita, prefira não compartilhar (pedaços, filas) e fique atento a deadlocks.

A seguir: multiprocessing, usar vários processos para pôr cada núcleo da CPU para trabalhar.

Lab: o que é avaliado

  • Repartir o trabalho com split_chunks25 pts
  • Aplicar uma função aos pedaços com run_in_chunks25 pts
  • Proteger dados compartilhados com safe_add25 pts
  • Saber o que as threads ganham com estimate_time25 pts
Explorador · lab-125-1
▾ lab-125-1
…
bash · no navegador
learner@quantcraft:~/lab-125-1$
Python ainda não carregadoPythonLn 1, Col 1Spaces: 4UTF-8

Redefinir este tópico?

Tudo em lab-125-1 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

Entre para executar o seu código.

Todas as aulas são gratuitas para ler. Crie uma conta ou entre para executar código, rodar os testes e enviar este lab.

  • IDE no navegador com terminal bash — nada para instalar
  • Correção instantânea, XP e o seu lugar no roteiro
Criar uma contaEntrar
OU
Continuar com o GitHub