Lección 0.4.6 · 55 min
Concurrencia: threading, multiprocessing, asyncio y free-threading
Un sistema de trading pasa casi todo su tiempo esperando: a la red, al bróker, al siguiente tick. Otras veces calcula sin parar: simulaciones, optimizaciones. Cada caso tiene su herramienta en Python, y elegir mal puede no acelerar nada. Esta lección las compara con ejemplos deterministas y explica qué cambia con el Python sin GIL.
- Módulo
- 0.4 · Python intermedio
- Duración
- 55 min
Lecciones del nivel 0
0.1 · Bienvenida y expectativas
0.2 · Entorno de trabajo moderno
- 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
- 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
- 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
- 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
- 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
0.3 · Fundamentos del lenguaje
- 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
- 0.3.2 Cadenas, f-strings y formato de números y fechas
- 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
- 0.3.4 Control de flujo: if, elif, else, for, while y match-case
- 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
- 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
0.4 · Python intermedio
- 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
- 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
- 0.4.3 Iteradores, generadores, decoradores y context managers
- 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
- 0.4.5 Logging profesional y testing con pytest
- 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
0.5 · Computación numérica y datos
- 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
- 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
- 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
- 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
- 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
0.6 · Visualización
0.7 · Matemáticas y estadística mínimas
Esperar o calcular
Un programa puede ir lento por dos motivos muy distintos:
- Espera (limitado por E/S, entrada/salida): pedir precios a una API, recibir mensajes de un websocket, leer ficheros, escribir en una base de datos. El procesador está casi parado; el tiempo se va en esperar respuestas.
- Calcula (limitado por CPU): simular miles de trayectorias, optimizar parámetros, recalcular indicadores sobre años de datos. El procesador está al 100 %.
La concurrencia consiste en tener varias tareas en marcha a la vez; el paralelismo, en ejecutarlas literalmente al mismo tiempo en varios núcleos. Para esperar basta con concurrencia: mientras una petición espera, se lanza otra. Para calcular hace falta paralelismo. Python ofrece tres herramientas, y una cuarta que cambia las reglas:
| Herramienta | Qué crea | Paralelismo real de código Python | Encaja con |
|---|---|---|---|
threading / ThreadPoolExecutor | Hilos en un proceso | No (con GIL); sí en 3.14t | Esperas: APIs REST, ficheros |
multiprocessing / ProcessPoolExecutor | Procesos con su memoria | Sí | Cálculo: simulaciones, optimizaciones |
asyncio | Corrutinas en un hilo | No | Muchas esperas: websockets, varios exchanges |
| Free-threading (3.14t) | Hilos sin GIL | Sí | Cálculo con memoria compartida |
Antes de todo esto hay una opción que no aparece en la tabla: vectorizar. Un cálculo que NumPy o Polars hacen sobre arrays enteros (lecciones 0.5.1 y 0.5.4) corre en C, suele ser decenas de veces más rápido que un bucle de Python y a menudo hace innecesario el paralelismo.
Todos los ejemplos de esta lección son deterministas: esperas simuladas con sleep, simulaciones con semilla y
resultados que se recogen en el orden pedido. El orden en que terminan las tareas, en cambio, puede cambiar de una
ejecución a otra, y ningún resultado debe depender de él.
Hilos para esperar a la red
La función descargar simula la petición del último cierre de un activo: espera un cuarto de segundo, como una
llamada HTTP, y devuelve los cierres del 31 de diciembre de 2025 de cartera_sint.csv. Cuatro peticiones seguidas
tardan cuatro cuartos de segundo:
import time
CIERRES = {"RVAR": 164.35, "RFIJ": 102.29, "ORO": 171.44,
"TECH": 77.82}
def descargar(simbolo: str) -> tuple[str, float]:
"""Simula una petición HTTP: 0,25 s de espera de red."""
time.sleep(0.25)
return simbolo, CIERRES[simbolo]
simbolos = list(CIERRES)
t0 = time.perf_counter()
secuencial = [descargar(s) for s in simbolos]
t_secuencial = time.perf_counter() - t0
print(secuencial)[('RVAR', 164.35), ('RFIJ', 102.29), ('ORO', 171.44), ('TECH', 77.82)]Mientras descargar espera, el programa no hace nada. Con hilos, las cuatro esperas se solapan.
concurrent.futures.ThreadPoolExecutor mantiene un grupo de hilos y les reparte las tareas; su método map funciona
como el map normal y devuelve los resultados en el orden de la entrada, aunque las tareas terminen en otro:
from concurrent.futures import ThreadPoolExecutor
t0 = time.perf_counter()
with ThreadPoolExecutor(max_workers=4) as pool:
en_hilos = list(pool.map(descargar, simbolos))
t_hilos = time.perf_counter() - t0
print(en_hilos == secuencial)
print("al menos el doble de rápido:", t_hilos < t_secuencial / 2)True
al menos el doble de rápido: TrueLos resultados son idénticos y el tiempo total se acerca al de una sola descarga. La lección no imprime los tiempos
porque cambian en cada ejecución; la comparación, con un margen amplio, no. El with cierra el grupo de hilos al
salir y espera a que terminen todas las tareas (lección 0.4.3).
Con una API real, el número de hilos lo limita el proveedor, no el ordenador: casi todas las APIs de datos y de brókeres fijan un máximo de peticiones por segundo, y superarlo lleva a errores 429 o a un bloqueo temporal.
Errores y orden
submit lanza una tarea y devuelve un futuro, un objeto que tendrá el resultado cuando la tarea termine.
futuro.result() lo entrega o, si la tarea lanzó una excepción, la vuelve a lanzar en el hilo que pregunta. Recorrer
los futuros en el orden en que se lanzaron mantiene la salida estable y permite tratar cada error por separado:
with ThreadPoolExecutor(max_workers=4) as pool:
futuros = {s: pool.submit(descargar, s)
for s in ["ORO", "XYZ", "TECH"]}
for simbolo, futuro in futuros.items():
try:
print(simbolo, futuro.result()[1])
except KeyError as e:
print(simbolo, "error:", repr(e))ORO 171.44
XYZ error: KeyError('XYZ')
TECH 77.82Un fallo no tumba las demás descargas. concurrent.futures.as_completed(futuros) entregaría los futuros según
terminan, útil para mostrar progreso, pero en un orden que puede cambiar en cada ejecución.
Estado compartido: Lock
Los hilos comparten la memoria del proceso. Si varios modifican la misma variable a la vez, una actualización puede
pisar a otra: exposicion += importe lee, suma y escribe en pasos separados, y otro hilo puede colarse entre ellos.
El error es intermitente, depende de la carga de la máquina y casi nunca sale en las pruebas. La protección es un
threading.Lock: solo un hilo a la vez ejecuta el bloque with cerrojo:
import threading
exposicion = 0.0
cerrojo = threading.Lock()
def registrar_ejecucion(importe: float) -> None:
global exposicion
with cerrojo:
exposicion += importe
importes = [7_782.0, 17_144.0, -3_500.0, 10_229.0] * 2_500
with ThreadPoolExecutor(max_workers=8) as pool:
list(pool.map(registrar_ejecucion, importes))
print(f"{exposicion:,.2f}", f"{sum(importes):,.2f}")79,137,500.00 79,137,500.00Con el cerrojo, el total coincide siempre con la suma directa. El diseño más seguro, de todos modos, es no compartir: que cada tarea devuelva su resultado y que el hilo principal los sume al final, como en las descargas.
Procesos para calcular
Los hilos no aceleran el cálculo en Python con GIL (el Global Interpreter Lock): un cerrojo dentro del intérprete que deja ejecutar código Python a un solo hilo a la vez. Los hilos lo sueltan mientras esperan (por eso funcionan con la red), pero no mientras calculan. Para usar varios núcleos con código Python puro, la solución clásica son varios procesos, cada uno con su intérprete, su GIL y su memoria.
El cálculo de ejemplo es una simulación de Monte Carlo: para cada semilla, 2.000 años simulados de 252 sesiones con rentabilidades normales, y el peor drawdown encontrado. Es Python puro, sin NumPy, a propósito:
import random
def peor_drawdown(semilla: int, anios: int = 2_000) -> float:
"""Peor drawdown en `anios` años simulados de 252 sesiones."""
rng = random.Random(semilla)
peor = 0.0
for _ in range(anios):
valor = pico = 1.0
for _ in range(252):
valor *= 1 + rng.gauss(0.0004, 0.012)
pico = max(pico, valor)
peor = min(peor, valor / pico - 1)
return peor
semillas = [1, 2, 3, 4]
peores = [peor_drawdown(s) for s in semillas]
print([f"{p:.2%}" for p in peores])['-49.04%', '-44.49%', '-49.17%', '-48.19%']Con una volatilidad diaria del 1,2 % (un 19 % anual), el peor de 2.000 años simulados pierde entre el 44 % y el 49 % desde su máximo según la semilla: los años malos de verdad son raros, pero existen. Cada semilla tarda una fracción de segundo; con cientos de semillas, el reparto entre núcleos empieza a importar.
Con multiprocessing, un Pool de procesos reparte las semillas y map devuelve los resultados en orden. Aquí entra
una novedad de Python 3.14: en Linux, el método de arranque por defecto pasó de fork a forkserver.1Nota 1 · Documentación de multiprocessing en Python 3.14: «On POSIX platforms the default start method was changed
from fork to forkserver to retain the performance but avoid common multithreaded process incompatibilities». En
macOS y Windows sigue siendo spawn.
forkcopia el proceso padre tal cual, con todas sus funciones y datos. Es rápido, pero copiar un proceso que tiene hilos en marcha puede dejar el hijo bloqueado, y por eso dejó de ser el valor por defecto.forkserverarranca al principio un proceso servidor limpio, de un solo hilo, y crea los hijos a partir de él. Los hijos no heredan las funciones definidas en el programa: las reciben por nombre y las importan, así que tienen que estar en un módulo importable.spawnarranca un intérprete nuevo para cada hijo. Es el valor por defecto en Windows y macOS.
En un notebook, las funciones definidas en una celda no están en ningún fichero importable, así que con forkserver
o spawn el pool falla. La salida en un cuaderno es pedir fork explícitamente con un contexto:
import multiprocessing as mp
print("por defecto:", mp.get_start_method())
ctx = mp.get_context("fork")
with ctx.Pool(processes=4) as pool:
en_procesos = pool.map(peor_drawdown, semillas)
print(en_procesos == peores)por defecto: forkserver
TrueMismos resultados, porque cada simulación depende solo de su semilla, no del proceso que la ejecuta ni del momento en que termina. Sin semillas por tarea, cada proceso generaría números distintos en cada ejecución.
En un script, la forma recomendada en cualquier plataforma es la otra: las funciones en el nivel superior del fichero
y el arranque bajo if __name__ == "__main__":, para que los hijos puedan importar el fichero sin volver a lanzar el
pool. Con ProcessPoolExecutor, la interfaz es la misma que con hilos:
from concurrent.futures import ProcessPoolExecutor
from montecarlo import peor_drawdown # la función, en su módulo
def main() -> None:
semillas = range(1, 33)
with ProcessPoolExecutor() as pool: # un proceso por núcleo
peores = list(pool.map(peor_drawdown, semillas))
print(f"peor de {len(peores)} simulaciones: {min(peores):.2%}")
if __name__ == "__main__":
main()Los procesos tienen costes que los hilos no tienen: arrancar cada uno lleva su tiempo, no comparten memoria y todo lo
que entra o sale de ellos se serializa con pickle. Compensan cuando cada tarea calcula bastante y recibe y devuelve
poco, como una simulación que devuelve un número. Repartir entre procesos un DataFrame de millones de filas para hacer
una suma suele ir más lento que hacerla en uno.
asyncio: muchas esperas en un solo hilo
asyncio resuelve la espera de otra forma. En lugar de varios hilos, un solo hilo ejecuta un bucle de eventos que
alterna entre corrutinas: funciones definidas con async def que, al llegar a un await, ceden el control
mientras esperan. Es la base de los clientes de websockets y de las APIs asíncronas de casi todos los exchanges, y de
los sistemas que escuchan muchos flujos de datos a la vez (nivel 3).
Tres exchanges ficticios, con latencias distintas, devuelven la mejor compra (bid) y la mejor venta (ask) de BTC.
asyncio.gather lanza las tres consultas a la vez y devuelve los resultados en el orden de los argumentos:
import asyncio
LIBROS = { # exchange: (latencia en s, bid, ask), ficticios
"alfa": (0.30, 67_412.5, 67_415.0),
"beta": (0.10, 67_418.0, 67_421.5),
"gamma": (0.20, 67_409.0, 67_410.5),
}
async def mejor_precio(exchange: str) -> tuple[str, float, float]:
latencia, bid, ask = LIBROS[exchange]
await asyncio.sleep(latencia) # la petición de red
return exchange, bid, ask
async def consultar() -> list[tuple[str, float, float]]:
return await asyncio.gather(*(mejor_precio(e) for e in LIBROS))
cotizaciones = asyncio.run(consultar())
for c in cotizaciones:
print(c)
compra = min(cotizaciones, key=lambda c: c[2])
venta = max(cotizaciones, key=lambda c: c[1])
print(f"comprar en {compra[0]} a {compra[2]:,.1f}, "
f"vender en {venta[0]} a {venta[1]:,.1f}")('alfa', 67412.5, 67415.0)
('beta', 67418.0, 67421.5)
('gamma', 67409.0, 67410.5)
comprar en gamma a 67,410.5, vender en beta a 67,418.0beta responde primero y alfa el último, pero gather devuelve los resultados en el orden pedido. El total tarda lo
que la consulta más lenta, no la suma de las tres. Comprar en gamma a 67.410,5 y vender en beta a 67.418 deja
7,5 dólares por BTC antes de comisiones, que en la práctica se las llevarían enteras: es el tipo de cálculo que hace un
arbitrajista, y la razón de que la latencia importe.
asyncio.run crea el bucle, ejecuta la corrutina y lo cierra: es la entrada de un programa asíncrono. Jupyter y
Marimo ya tienen un bucle en marcha, y en ellos se escribe directamente await consultar() en la celda.
Un exchange lento no debe retrasar a los demás. asyncio.timeout pone un tiempo límite a una espera y lanza
TimeoutError si se supera:
LIBROS["delta"] = (2.0, 67_420.0, 67_423.0) # muy lento
async def con_limite(exchange: str, limite: float = 0.5):
try:
async with asyncio.timeout(limite):
return await mejor_precio(exchange)
except TimeoutError:
return exchange, None, None
async def consultar_todos():
return await asyncio.gather(*(con_limite(e) for e in LIBROS))
for c in asyncio.run(consultar_todos()):
print(c)('alfa', 67412.5, 67415.0)
('beta', 67418.0, 67421.5)
('gamma', 67409.0, 67410.5)
('delta', None, None)delta se descarta a los 0,5 s y el resto llega a tiempo. Para lanzar tareas que deben cancelarse juntas si una
falla, Python 3.11+ ofrece asyncio.TaskGroup, la forma estructurada de hacer lo mismo que gather.
El GIL y el Python sin GIL
El GIL simplificó durante décadas el intérprete y las extensiones en C, a cambio de que los hilos no pudieran calcular en paralelo. El PEP 703 propuso hacerlo opcional y se aceptó con la condición de un despliegue gradual y reversible.2Nota 2 · PEP 703, estado Final. El Steering Council lo aceptó «with clear proviso: that the rollout be gradual and break as little as possible». Python 3.13 incluyó una compilación experimental sin GIL, y en Python 3.14 esa compilación pasó a estar soportada oficialmente (PEP 779), aunque sigue siendo opcional: el Python que se instala por defecto lleva GIL.3Nota 3 · PEP 779, estado Final, aceptado el 16 de junio de 2025; «What's New In Python 3.14»: «Free-threaded Python is officially supported» y «The performance penalty on single-threaded code in free-threaded mode is now roughly 5-10%» (https://docs.python.org/3.14/whatsnew/3.14.html). Hacerla la opción por defecto queda para un PEP futuro.
La compilación sin GIL se llama free-threading y su ejecutable lleva una t (python3.14t). Con ella, los hilos
ejecutan código Python en paralelo, y una simulación como la de la lección escala con hilos, sin los costes de los
procesos. Tiene contrapartidas: según las notas de la versión, el código de un solo hilo va entre un 5 % y un 10 % más
lento, y una extensión en C que no declare compatibilidad vuelve a activar el GIL al importarse, con un aviso.
Desde Python se comprueba qué compilación se está usando y si el GIL está activo:
import sys
import sysconfig
print(sys.version_info[:3])
print("compilación free-threading:",
bool(sysconfig.get_config_var("Py_GIL_DISABLED")))
print("GIL activo:", sys._is_gil_enabled())(3, 14, 8)
compilación free-threading: False
GIL activo: TrueEl curso usa la compilación normal, con GIL. Para probar la otra, uv instala las dos variantes:4Nota 4 · Documentación de uv, «Python versions»: uv instala variantes free-threaded desde CPython 3.13; en 3.14,
uv python install 3.14 sigue prefiriendo la compilación con GIL
(https://docs.astral.sh/uv/concepts/python-versions/).
uv python install 3.14t
uv run --python 3.14t python simular_hilos.py
PYTHON_GIL=1 uv run --python 3.14t python simular_hilos.py # GIL forzadoCon free-threading, el código que comparte estado entre hilos necesita más que nunca los Lock: sin GIL, las
condiciones de carrera que antes eran raras se vuelven frecuentes.
Qué usar en cada caso
El árbol de decisión, de arriba abajo:
Fuente Mermaid
flowchart TB
Q{¿Espera o calcula?} -->|espera| N{¿Muchas a la vez?}
N -->|pocas| T[Hilos]
N -->|cientos| A[asyncio]
Q -->|calcula| V{¿Se vectoriza?}
V -->|sí| NP[NumPy o Polars]
V -->|no| P[Procesos]
P -.->|3.14t| H[Hilos sin GIL]Para pedir precios de unas decenas de símbolos a una API REST, hilos; para mantener abiertos cientos de websockets, asyncio; para un cálculo que se puede expresar con arrays, NumPy o Polars antes que cualquier otra cosa; para un cálculo en Python puro que no se vectoriza (simulaciones con lógica de trading barra a barra, optimizaciones), procesos, o hilos en una compilación sin GIL. Y en todos los casos, resultados que no dependan del orden en que terminan las tareas.
Ejercicios
- Repite las descargas con
max_workers=2. ¿Cambian los resultados? ¿Qué esperas que pase con el tiempo total?
Solución
t0 = time.perf_counter()
with ThreadPoolExecutor(max_workers=2) as pool:
dos_hilos = list(pool.map(descargar, simbolos))
t_dos = time.perf_counter() - t0
print(dos_hilos == secuencial)
print("más lento que con 4 hilos:", t_dos > t_hilos)True
más lento que con 4 hilos: TrueLos resultados no cambian. Con dos hilos, las cuatro descargas se hacen en dos tandas: unos 0,5 s, el doble que con
cuatro y la mitad que en secuencia. max_workers es el número de peticiones simultáneas, el parámetro que hay que
ajustar al límite de la API.
- Un exchange admite como máximo dos peticiones simultáneas. Con
asyncio.Semaphore(2), consultaalfa,betaygammasin pasar de dos a la vez y comprueba que el resultado es el mismo que antes.
Solución
async def consultar_con_limite(maximo: int = 2):
semaforo = asyncio.Semaphore(maximo)
async def una(exchange: str):
async with semaforo: # espera turno si ya hay `maximo`
return await mejor_precio(exchange)
nombres = ["alfa", "beta", "gamma"]
return await asyncio.gather(*(una(e) for e in nombres))
print(asyncio.run(consultar_con_limite()) == cotizaciones)TrueEl semáforo deja pasar a dos corrutinas y hace esperar a la tercera hasta que una termina. Es la herramienta habitual para respetar los límites de peticiones de una API asíncrona.
- Con el pool de procesos, lanza
peor_drawdownpara las semillas 1 a 4 con 500 años en lugar de 2.000 (usapool.starmapcon pares(semilla, anios)). ¿Por qué el peor drawdown es menos extremo?
Solución
with ctx.Pool(processes=4) as pool:
cortos = pool.starmap(peor_drawdown, [(s, 500) for s in semillas])
print([f"{p:.2%}" for p in cortos])['-49.04%', '-41.01%', '-43.26%', '-38.45%']starmap desempaqueta cada tupla como argumentos. Con menos años simulados hay menos ocasiones de encontrar una racha
mala: el peor caso de 500 años es como mucho igual de extremo que el de 2.000, y casi siempre menos. Es la misma razón
por la que un backtest corto subestima el drawdown que una estrategia puede llegar a tener.
Footnotes
-
Documentación de
multiprocessingen Python 3.14: «On POSIX platforms the default start method was changed from fork to forkserver to retain the performance but avoid common multithreaded process incompatibilities». En macOS y Windows sigue siendospawn. ↩ -
PEP 703, estado Final. El Steering Council lo aceptó «with clear proviso: that the rollout be gradual and break as little as possible». ↩
-
PEP 779, estado Final, aceptado el 16 de junio de 2025; «What's New In Python 3.14»: «Free-threaded Python is officially supported» y «The performance penalty on single-threaded code in free-threaded mode is now roughly 5-10%» (https://docs.python.org/3.14/whatsnew/3.14.html). ↩
-
Documentación de uv, «Python versions»: uv instala variantes free-threaded desde CPython 3.13; en 3.14,
uv python install 3.14sigue prefiriendo la compilación con GIL (https://docs.astral.sh/uv/concepts/python-versions/). ↩
Respuesta plegada en cada pregunta
Autoevaluación
Hay que pedir el último precio de 200 símbolos a una API REST. ¿Qué herramienta encaja mejor?
- A
multiprocessing, con un proceso por símbolo - BHilos o asyncio: la tarea es esperar a la red, y mientras un hilo espera los demás pueden lanzar sus peticiones
- CUn bucle secuencial: Python no puede hacer dos cosas a la vez
- DNumPy
Ver respuesta
Respuesta B. Hilos o asyncio: la tarea es esperar a la red, y mientras un hilo espera los demás pueden lanzar sus peticiones
Es una tarea de E/S: casi todo el tiempo se va en esperar respuestas. Durante la espera el GIL se libera, así que los hilos (o las corrutinas) se solapan. Doscientos procesos costarían mucha más memoria sin ninguna ventaja.
- A
¿En qué orden devuelve los resultados
ThreadPoolExecutor.map(f, simbolos)?- AEn el orden en que terminan las tareas
- BEn el orden de
simbolos, aunque las tareas terminen en otro orden - CAl azar
- DOrdenados alfabéticamente
Ver respuesta
Respuesta B. En el orden de
simbolos, aunque las tareas terminen en otro ordenmapconserva el orden de la entrada.as_completed, en cambio, entrega los futuros según terminan, un orden que puede cambiar en cada ejecución.En Python 3.14 sobre Linux, ¿cuál es el método de arranque por defecto de
multiprocessing?- A
fork - B
spawn - C
forkserver - DDepende de la CPU
Ver respuesta
Respuesta C.
forkserverPython 3.14 cambió el valor por defecto de
forkaforkserveren Linux y el resto de Unix salvo macOS, donde siguespawn. La función que se envía a otro proceso tiene que poder importarse desde un módulo; paraforkhay que pedirlo conget_context("fork").- A
¿Qué hace el modo free-threading (compilación
3.14t)?- AEjecuta las corrutinas de asyncio en paralelo
- BElimina el GIL, de modo que varios hilos pueden ejecutar código Python en paralelo en varios núcleos
- CSustituye a multiprocessing en todas las plataformas
- DAcelera el código de un solo hilo
Ver respuesta
Respuesta B. Elimina el GIL, de modo que varios hilos pueden ejecutar código Python en paralelo en varios núcleos
Sin GIL, los hilos de código Python puro pueden usar varios núcleos. Tiene un coste: según las notas de la 3.14, el código de un solo hilo va un 5–10 % más lento, y algunas extensiones en C vuelven a activar el GIL.
Ideas clave
- Primero, la pregunta: ¿el programa espera (red, disco) o calcula (CPU)? Para esperar, hilos o asyncio; para calcular, NumPy, procesos o, en 3.14t, hilos sin GIL.
ThreadPoolExecutor.mapyasyncio.gatherdevuelven los resultados en el orden pedido. El orden de finalización no es reproducible: no dependas de él.- El estado compartido entre hilos se protege con un
Lock. Mejor aún, que cada tarea devuelva su resultado y se combinen al final. - En Python 3.14
multiprocessingarranca conforkserveren Linux: las funciones tienen que estar en un módulo importable y el script, bajoif __name__ == "__main__":. - asyncio maneja miles de conexiones en un solo hilo, siempre que nada bloquee el bucle: un
time.sleepdentro de una corrutina lo para todo.
Recursos
Documentación
- Documentación de Python 3.14: concurrent.futures
- Documentación de Python 3.14: multiprocessing (métodos de arranque)
- Documentación de Python 3.14: asyncio
- PEP 703: Making the Global Interpreter Lock Optional in CPython
- PEP 779: Criteria for supported status for free-threaded Python
- Python 3.14: guía de uso del modo free-threading