Skip to content
SP StackPractices
advanced

Guía de Patrones de Concurrencia

Guía de patrones de concurrencia comunes y mejores prácticas para escribir código concurrente seguro y eficiente.

Resumen

La concurrencia permite que los programas manejen múltiples tareas simultáneamente. Usada correctamente, mejora el throughput y la capacidad de respuesta. Usada incorrectamente, introduce race conditions, deadlocks y bugs sutiles que son difíciles de reproducir.

Cuándo Usar Concurrencia

Caso de UsoEnfoque
Tareas I/O-boundAsync/await, coroutines
Tareas CPU-boundThread pools, multiprocessing
Jobs en backgroundColas de tareas

Thread Pool Pattern

En vez de crear threads por tarea, reutiliza un pool fijo.

from concurrent.futures import ThreadPoolExecutor

def fetch(url):
    return requests.get(url, timeout=10).status_code

with ThreadPoolExecutor(max_workers=5) as executor:
    results = list(executor.map(fetch, urls))

Regla general: Tamaño del pool ~ número de cores de CPU para tareas CPU-bound, mayor para I/O-bound.

Async/Await Pattern

I/O no bloqueante sin threads.

import asyncio

async def fetch_all(urls):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        return await asyncio.gather(*tasks)

Producer-Consumer Pattern

Desacopla la generación de trabajo del procesamiento.

import asyncio
from asyncio import Queue

async def producer(queue: Queue, items: list):
    for item in items:
        await queue.put(item)
    await queue.put(None)

async def consumer(queue: Queue):
    while True:
        item = await queue.get()
        if item is None: break
        # ... procesar item
        queue.task_done()

Semáforo para Rate Limiting

Controla el acceso a recursos limitados.

class RateLimitedClient:
    def __init__(self, max_concurrent: int = 5):
        self.semaphore = asyncio.Semaphore(max_concurrent)

    async def request(self, url: str):
        async with self.semaphore:
            return await fetch(url)

Evitando Race Conditions

Datos Inmutables

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: float
    y: float

Operaciones Atómicas

import threading

class SafeCounter:
    def __init__(self):
        self._value = 0
        self._lock = threading.Lock()

    def increment(self):
        with self._lock:
            self._value += 1

Errores Comunes

ProblemaSíntomaSolución
Race conditionResultados incorrectos intermitentesLocks, operaciones atómicas, inmutabilidad
DeadlockThreads congelados esperándoseOrden consistente de locks, timeouts
Thread leakMemoria crece con el tiempoUsar thread pools, siempre hacer shutdown
Context switchingCPU alta, throughput bajoReducir cantidad de threads, usar async I/O

Buenas Prácticas

  • Share nothing: Prefiere paso de mensajes sobre estado compartido
  • Usa colecciones thread-safe: ConcurrentHashMap, Queue, AtomicInteger
  • Mantén secciones críticas pequeñas: Bloquea por el mínimo tiempo
  • Nunca llames APIs externas mientras mantienes un lock

Preguntas Frecuentes

Cuándo debería usar async/await vs threads?

Usa async/await para tareas I/O-bound (HTTP calls, sistema de archivos, bases de datos). Usa threads o procesos para trabajo CPU-bound (cálculos, procesamiento de datos) que necesita ejecución paralela.

Cómo evito deadlocks?

Siempre adquiere locks en el mismo orden en tu codebase. Usa timeouts en adquisición de locks. Prefiere estructuras de datos lock-free cuando sea posible. La solución más simple a menudo es reducir el estado compartido.

Cuál es la diferencia entre concurrencia y paralelismo?

La concurrencia es sobre estructurar un programa para manejar múltiples tareas (intercalación). El paralelismo es sobre ejecutar múltiples tareas simultáneamente (realmente al mismo tiempo). Async I/O es concurrente; multithreading en múltiples cores es paralelo.