8 - Errores Comunes y Cómo Evitarlos

La concurrencia ofrece mejoras de respuesta y paralelismo, pero también introduce errores difíciles de depurar. Estas son las trampas más habituales y cómo esquivarlas.
La clave es medir, limitar la cantidad de workers y aplicar patrones seguros de sincronización.

8.1 Crear demasiados hilos en tareas I/O-bound

  • Problemas de escalabilidad: miles de hilos consumen pila, generan cambios de contexto y pueden agotar descriptores.
    El sistema operativo gastará tiempo en planificar en lugar de avanzar.
  • Límites razonables: empieza con 5-50 hilos según latencia y RAM; mide y ajusta. Considera asyncio si necesitas miles de sockets.
    Los pools ayudan a fijar un límite estable de workers.

8.2 Crear demasiados procesos en tareas CPU-bound

  • Contención del scheduler: más procesos que núcleos generan competencia y más cambios de contexto.
    La caché de CPU se invalida con frecuencia y el rendimiento cae.
  • Oversubscription: usar 2-4x núcleos suele empeorar el rendimiento. Apunta a cpu_count() o ligeramente menos.
    Pools con tamaño fijo evitan reventar el scheduler.

8.3 Bloqueos y deadlocks entre hilos

  • Buenas prácticas: adquiere locks en orden fijo, libera siempre (context managers), evita locks anidados si es posible, y usa timeouts para detectar bloqueos.
    Prefiere diseños sin estado compartido y usa colas antes que locks cuando sea viable.

8.4 Problemas de serialización con ProcessPool

  • Funciones no picklables: define funciones a nivel de módulo; evita lambdas y cierres que capturen estado.
    En Windows (spawn) esto es obligatorio o fallará al iniciar el proceso.
  • Objetos complejos: clases con referencias a recursos (sockets, archivos abiertos) no viajan bien por pickle.
    Pasa solo datos serializables y reabre recursos dentro del worker.

8.5 Uso incorrecto de shared_memory

  • Falta de sincronización: escribir en posiciones compartidas sin locks o partición clara causa corrupción.
    Divide rangos para evitar que dos procesos toquen el mismo índice.
  • Accesos desalineados: leer/escribir con el tipo equivocado desincroniza bytes; usa casts correctos y planifica offsets.
    Mantén un solo tipo y tamaño por segmento.

8.6 Aplicación: Detector de deadlocks y debugging con logging

El siguiente código simula un deadlock al adquirir locks en orden inverso, registra el orden de adquisición y luego muestra una versión corregida con orden consistente y timeout.

import logging
import threading
import time

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(threadName)s] %(message)s",
    datefmt="%H:%M:%S",
)

lock_a = threading.Lock()
lock_b = threading.Lock()


def tarea_mala():
    logging.debug("Intentando lock A")
    with lock_a:
        logging.debug("Tomó lock A, dormir")
        time.sleep(0.5)
        logging.debug("Intentando lock B")
        with lock_b:
            logging.debug("Nunca debería llegar aquí (deadlock)")


def tarea_mala_inversa():
    logging.debug("Intentando lock B")
    with lock_b:
        logging.debug("Tomó lock B, dormir")
        time.sleep(0.5)
        logging.debug("Intentando lock A")
        with lock_a:
            logging.debug("Nunca debería llegar aquí (deadlock)")


def tarea_buena():
    # Orden consistente: siempre A luego B
    acquired_a = lock_a.acquire(timeout=1)
    if not acquired_a:
        logging.debug("No se pudo tomar lock A, abortando")
        return
    try:
        logging.debug("Tomó lock A, dormir")
        time.sleep(0.2)
        acquired_b = lock_b.acquire(timeout=1)
        if not acquired_b:
            logging.debug("Timeout en lock B, liberando A")
            return
        try:
            logging.debug("Trabajo hecho sin deadlock")
        finally:
            lock_b.release()
    finally:
        lock_a.release()


if __name__ == "__main__":
    h1 = threading.Thread(target=tarea_mala, name="mala-A-primero")
    h2 = threading.Thread(target=tarea_mala_inversa, name="mala-B-primero")
    h1.start()
    h2.start()
    time.sleep(1.5)  # deja que se bloquee
    logging.debug("Los hilos quedaron bloqueados (deadlock)")

    # Versión corregida
    lock_a = threading.Lock()
    lock_b = threading.Lock()
    buenos = [threading.Thread(target=tarea_buena, name=f"bueno-{i}") for i in range(2)]
    for h in buenos:
        h.start()
    for h in buenos:
        h.join()
    logging.debug("Terminó versión corregida sin deadlocks")

El logging muestra cómo dos hilos se bloquean mutuamente al tomar locks en distinto orden. La versión corregida impone un orden fijo y timeouts para salir sin quedar atrapados, exponiendo el estado en los logs para depurar.
Aplica el mismo principio en código real: orden consistente de locks, timeouts y un canal de logging con timestamps.