threading permite ejecutar varias tareas concurrentes dentro de un mismo proceso de Python. Es especialmente útil cuando un programa pasa tiempo esperando: descargas, solicitudes HTTP, acceso a archivos, consultas de red o interacción con servicios externos. Mientras un hilo espera una respuesta, otro puede avanzar. Esto ayuda a evitar interfaces congeladas y a reducir el tiempo total de muchas operaciones de entrada y salida.
Los hilos no son una solución universal para acelerar código. En tareas intensivas de CPU, el GIL limita la ejecución paralela de bytecode en la implementación estándar de Python. Antes de elegir una estrategia, revisa la explicación del GIL de Python.
Crear tu primer hilo
Un hilo recibe una función objetivo y sus argumentos. Después se inicia con start().
from threading import Thread
import time
def descargar(nombre: str) -> None:
print(f"Iniciando {nombre}")
time.sleep(2)
print(f"Finalizado {nombre}")
hilo = Thread(target=descargar, args=("archivo.zip",))
hilo.start()
print("El hilo principal sigue disponible")
hilo.join()
print("Todo terminó")join() hace que el hilo actual espere hasta que el hilo indicado finalice. Sin esa espera, el programa puede continuar a una fase que depende de resultados todavía incompletos.
Ejecutar varias tareas concurrentes
from threading import Thread
import time
def procesar(nombre: str) -> None:
print(f"Procesando {nombre}")
time.sleep(1)
print(f"Listo: {nombre}")
hilos = []
for nombre in ["a.csv", "b.csv", "c.csv"]:
hilo = Thread(target=procesar, args=(nombre,))
hilo.start()
hilos.append(hilo)
for hilo in hilos:
hilo.join()Las tres esperas simuladas se solapan. Sin embargo, crear miles de hilos puede agotar memoria y recursos del sistema. Para controlar la cantidad de trabajadores es mejor utilizar un pool.
ThreadPoolExecutor
ThreadPoolExecutor, del módulo concurrent.futures, simplifica la administración de hilos y permite limitar la concurrencia.
from concurrent.futures import ThreadPoolExecutor
import time
def consultar(url: str) -> str:
time.sleep(1)
return f"Respuesta de {url}"
urls = ["servicio-a", "servicio-b", "servicio-c", "servicio-d"]
with ThreadPoolExecutor(max_workers=4) as executor:
resultados = executor.map(consultar, urls)
for resultado in resultados:
print(resultado)El bloque with cierra el executor y espera a que terminen las tareas. La documentación de concurrent.futures describe map, submit, futuros, timeouts y propagación de excepciones.
Recuperar resultados y excepciones
from concurrent.futures import ThreadPoolExecutor, as_completed
def dividir(valor: int) -> float:
return 100 / valor
with ThreadPoolExecutor(max_workers=3) as executor:
futuros = [executor.submit(dividir, n) for n in [10, 0, 5]]
for futuro in as_completed(futuros):
try:
print(futuro.result())
except ZeroDivisionError as error:
print(f"Tarea inválida: {error}")Una excepción dentro de un trabajador no siempre aparece inmediatamente en el hilo principal. Al llamar a result(), la excepción se vuelve a lanzar y puede manejarse de forma explícita.
Condiciones de carrera
Una condición de carrera ocurre cuando varios hilos modifican un recurso compartido y el resultado depende del orden de ejecución. Incluso una operación que parece sencilla puede implicar varios pasos internos.
from threading import Lock, Thread
contador = 0
bloqueo = Lock()
def incrementar() -> None:
global contador
for _ in range(100_000):
with bloqueo:
contador += 1
hilos = [Thread(target=incrementar) for _ in range(4)]
for hilo in hilos:
hilo.start()
for hilo in hilos:
hilo.join()
print(contador)Lock permite que solo un hilo ejecute la sección crítica a la vez. Mantén esa sección lo más pequeña posible; un bloqueo sostenido durante una operación lenta elimina gran parte del beneficio de la concurrencia y puede crear interbloqueos.
Comunicación segura con Queue
En lugar de compartir listas y variables libremente, utiliza queue.Queue para enviar tareas o resultados entre hilos.
from queue import Queue
from threading import Thread
cola = Queue()
def trabajador() -> None:
while True:
elemento = cola.get()
try:
if elemento is None:
return
print(f"Procesando {elemento}")
finally:
cola.task_done()
hilo = Thread(target=trabajador)
hilo.start()
for valor in range(5):
cola.put(valor)
cola.put(None)
cola.join()
hilo.join()La cola implementa la sincronización necesaria y reduce la posibilidad de errores. Debes equilibrar cada get() con task_done(), incluso cuando exista una excepción.
Hilos daemon
Un hilo daemon no impide que el programa finalice cuando solo quedan hilos de ese tipo.
hilo = Thread(target=trabajo_secundario, daemon=True)
hilo.start()No uses daemon para operaciones que deban completar escrituras, guardar información o liberar recursos. El intérprete puede terminarlo sin una limpieza ordenada. Para tareas críticas, utiliza hilos normales y una estrategia explícita de cierre.
Cancelar y detener correctamente
Python no ofrece una función segura para matar arbitrariamente un hilo. Una opción es compartir un Event que indique cuándo debe terminar.
from threading import Event, Thread
import time
detener = Event()
def monitor() -> None:
while not detener.is_set():
print("Comprobando...")
detener.wait(timeout=1)
hilo = Thread(target=monitor)
hilo.start()
time.sleep(3)
detener.set()
hilo.join()Event.wait() puede despertar inmediatamente cuando se establece la señal, a diferencia de un sleep() rígido.
Threading, asyncio o multiprocessing
Usa hilos cuando trabajes con bibliotecas bloqueantes y tareas de entrada y salida. Considera asyncio cuando controles muchas conexiones compatibles con código asíncrono. Para cálculos intensivos que deben usar varios núcleos, utiliza procesos. No elijas por moda: mide la carga real.
Medir antes de optimizar
La concurrencia añade complejidad. Primero identifica el cuello de botella con la guía de cProfile y revisa las soluciones para un script Python lento. Si el tiempo se consume en CPU, los hilos probablemente no serán la respuesta.
Registrar actividad de varios hilos
Los mensajes de print() pueden mezclarse. El módulo logging incluye el nombre del hilo en el formato y administra la escritura de forma más adecuada.
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s | %(threadName)s | %(message)s",
)La guía de logging en Python muestra handlers, archivos y rotación.
Errores frecuentes
Los problemas más comunes son olvidar join(), compartir estado mutable sin sincronización, crear demasiados hilos, mantener un lock durante una operación lenta, ignorar excepciones de futuros y usar threading para tareas de CPU. También debes establecer timeouts en solicitudes de red; un hilo bloqueado indefinidamente puede impedir un cierre limpio.
Consulta la documentación oficial de threading para estudiar Thread, Lock, RLock, Semaphore, Condition, Event y barreras.
Conclusión
threading es una herramienta efectiva para mantener un programa receptivo y solapar esperas de entrada y salida. Empieza con ThreadPoolExecutor cuando necesites procesar una colección de tareas y recurre a las primitivas de bajo nivel solo cuando exista una necesidad clara. Reduce el estado compartido, utiliza colas, propaga excepciones y diseña una señal de cierre. La concurrencia bien aplicada mejora la experiencia; mal controlada crea errores intermitentes difíciles de reproducir.






