Threading en Python: evita que tus scripts se bloqueen

Publicado el: 11/07/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

    Uso de dataclasses para simplificar classes em Python
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Dataclasses en Python: clases de datos más limpias

    Aprende dataclasses en Python: campos, valores predeterminados, field, frozen, order, __post_init__, herencia, asdict y buenas prácticas.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026
    Criação de hashes seguros para senhas usando Python
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Hash de contraseñas con bcrypt en Python

    Aprende bcrypt en Python para almacenar contraseñas: salt, coste, hash, verificación, rehash, límites, errores y recomendaciones de seguridad.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Dicas para otimizar scripts Python lentos
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Por qué tu script Python es lento y cómo mejorarlo

    Descubre por qué un script Python es lento y cómo mejorarlo con cProfile, timeit, estructuras correctas, generadores y mejores algoritmos.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026
    Leitura de variáveis de ambiente em projetos Python
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Variables de entorno en Python: guía segura

    Aprende a usar variables de entorno en Python, archivos .env, validación, conversión de tipos, secretos, pruebas y buenas prácticas.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026
    Introdução ao uso de type hints em Python
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Type hints en Python: escribe código más claro

    Aprende type hints en Python con funciones, colecciones, Optional, TypedDict, Callable, genéricos, Protocol y herramientas de análisis.

    Ler mais

    Tempo de leitura: 4 minutos
    11/07/2026
    Medição de tempo de execução de código Python com timeit
    Buenas Prácticas
    Foto de perfil de Leandro Hirt da Academify

    Timeit en Python: medir el rendimiento correctamente

    Aprende timeit en Python para comparar funciones, repetir benchmarks, separar setup, medir por operación y evitar errores de microoptimización.

    Ler mais

    Tempo de leitura: 5 minutos
    11/07/2026