Threading en Python: hilos, locks y concurrencia

Actualizado el: 20/08/2026
Tempo de leitura: 4 minutos
Problemas de travamento com threading em scripts Python

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.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Configuración TOML en un proyecto Python
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    tomllib en Python: lee archivos TOML

    Aprende a leer TOML con tomllib en Python, validar configuraciones, gestionar errores y organizar proyectos con pyproject.toml.

    Ler mais

    Tempo de leitura: 6 minutos
    24/07/2026
    Desarrollador monitoreando registros estructurados en Python
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Observabilidad en Python con structlog

    Aprende observabilidad en Python con structlog, eventos JSON, contexto, pruebas, control de volumen y recomendaciones para producción.

    Ler mais

    Tempo de leitura: 5 minutos
    24/07/2026
    Configuración segura de aplicaciones Python con Pydantic Settings
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Pydantic Settings: configuración segura

    Aprende a validar variables de entorno, organizar configuraciones y proteger secretos en proyectos Python con Pydantic Settings.

    Ler mais

    Tempo de leitura: 5 minutos
    23/07/2026
    Desenvolvedor programando em Python com Ruff para lint e formatação
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Ruff: lint y formato de código Python

    Aprende Ruff en Python para lint, formato, correcciones automáticas, pyproject.toml, VS Code y CI con una configuración práctica.

    Ler mais

    Tempo de leitura: 10 minutos
    22/07/2026
    Dicas para melhorar performance de scripts Python lentos
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    ¿Por qué Python es lento? Causas y optimización

    Descubre por qué Python puede ser lento y mejora su rendimiento con cProfile, algoritmos, sets, generadores, NumPy, caché y concurrencia.

    Ler mais

    Tempo de leitura: 5 minutos
    12/07/2026
    Leitura segura de senhas no terminal usando Python
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    getpass: lee contraseñas en terminal con Python

    Lee contraseñas de forma segura con getpass, valida entradas, evita logs y texto plano y almacena credenciales con hashing adecuado.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026