σμ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 AcademyRutas
Ctrl K
AO
+1,2 k estudiantes
Iniciar sesiónUnirse ↗
Rutas
AO
+1,2 k estudiantes
Iniciar sesión
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Ruta de Python›Módulo 10 · Ingeniería Python de alto rendimiento›Tema 10.9·~55 minAvanzado

Hilos y el GIL

Posición en la rutaMódulo 10 de 11 · Tema 9 de 13
10.9 Hilos y el GIL
—Puntuación del lab— / 100XP total—Racha—

Hilos: concurrencia dentro de un proceso

Un hilo es una línea de ejecución independiente dentro de un mismo programa que comparte su memoria con los demás hilos. Un programa de trading que debe obtener cotizaciones de diez mercados, escribir un registro y mantener una interfaz receptiva usa hilos para que esperar a uno no detenga al resto. El módulo threading de Python y, más cómodamente, concurrent.futures.ThreadPoolExecutor los inician y gestionan. Como los hilos comparten variables, se comunican rápido y es peligroso equivocarse.

# 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

El GIL: hilos que esperan e hilos que calculan

CPython tiene un bloqueo global del intérprete (GIL): solo un hilo ejecuta bytecode de Python en cada instante. Un hilo lo cede mientras espera la red, un archivo o time.sleep, así que los hilos solapan muy bien las esperas: diez hilos que obtienen cotizaciones terminan en aproximadamente el tiempo de uno. Pero para código Python limitado por CPU (un bucle de Python puro con aritmética) los hilos se turnan, así que cuatro hilos no son más rápidos que uno y a menudo algo más lentos. El código que corre fuera de Python, como NumPy o una extensión de C que libera el GIL, puede correr en paralelo en hilos. Para cálculo pesado en Python puro usa procesos separados (siguiente tema) o la compilación sin GIL (free-threaded) de Python 3.13+, donde esté disponible.

# 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

Datos compartidos, condiciones de carrera y cerrojos

Los hilos que actualizan la misma variable pueden corromperla, porque total += amount son tres pasos (leer, sumar, escribir) y puede haber un cambio de hilo entre ellos: dos hilos leen 100, suman 5, escriben 105, y se pierde una actualización. Es una condición de carrera, que aparece raramente y de forma impredecible, lo que la hace el peor tipo de error. El remedio es un cerrojo: with lock: deja que un solo hilo a la vez ejecute el bloque, y tanto la lectura como la escritura deben estar dentro. Aún mejor, evita compartir: da a cada trabajador su propio trozo y su propio resultado y combina los resultados al final, o pasa trabajo y resultados por una queue.Queue, que ya es segura para hilos.

# 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
Los hilos son la herramienta equivocada más a menudo de lo que creesLos hilos encajan en E/S: muchas esperas lentas e independientes. Para muchas conexiones en un proceso, asyncio (visto antes en esta ruta) suele escalar mejor y no tiene carreras entre await. Para aritmética sobre arrays, vectoriza con NumPy. Para Python limitado por CPU, usa procesos. Si compartes datos entre hilos, mantén corta la sección bloqueada, nunca llames a código lento mientras sostienes un cerrojo y toma varios cerrojos siempre en el mismo orden, o dos hilos pueden esperarse para siempre (un interbloqueo).
  • Los hilos comparten memoria y solapan bien las esperas (red, archivos); un ThreadPoolExecutor puede consultar muchos mercados a la vez.
  • El GIL deja que un solo hilo ejecute bytecode de Python a la vez: sin aceleración para Python puro limitado por CPU, pero NumPy y las extensiones de C pueden liberarlo.
  • Protege las actualizaciones compartidas con with lock: alrededor de la lectura y la escritura, prefiere no compartir (trozos, colas) y vigila los interbloqueos.

A continuación: multiprocessing, usar varios procesos para poner a trabajar cada núcleo de la CPU.

Lab: qué se califica

  • Repartir el trabajo con split_chunks25 pts
  • Aplicar una función a los trozos con run_in_chunks25 pts
  • Proteger datos compartidos con safe_add25 pts
  • Saber lo que ganan los hilos con estimate_time25 pts
Explorador · lab-125-1
▾ lab-125-1
…
bash · en el navegador
learner@quantcraft:~/lab-125-1$
Python aún no cargadoPythonLn 1, Col 1Spaces: 4UTF-8

¿Restablecer este tema?

Todo lo que hay en lab-125-1 vuelve a los archivos iniciales y se limpia el terminal. Tu solución actual se perderá: no se puede deshacer.

Se restablecerá
  • main.py → código inicial
  • Historial del terminal y salida de pruebas
  • Cambios sin enviar
Se conservará
  • Mejor puntuación del lab
  • Tu XP y tu racha
  • Progreso de la lección y notas

Inicia sesión para ejecutar tu código.

Todas las clases se pueden leer gratis. Crea una cuenta o inicia sesión para ejecutar código, lanzar las pruebas y enviar este lab.

  • IDE en el navegador con terminal bash — sin instalar nada
  • Corrección instantánea, XP y tu lugar en la ruta
Crear una cuentaIniciar sesión
O
Continuar con GitHub