Volver al inicio

Fan-Out/Fan-In en Go: patrón para procesamiento de datos en paralelo

El artículo desglosa a fondo el patrón de procesamiento paralelo Fan-Out/Fan-In en el lenguaje Go. Cubre principios de operación, diferencias con Worker Pool, implementación práctica con ejemplos de código y recomendaciones clave para su uso en entornos de producción.

Paralelismo flexible en Go: diseccionando Fan-Out/Fan-In
Advertisement 728x90

# Patrón Fan-Out/Fan-In en Go: Procesamiento paralelo flexible de datos

El patrón Fan-Out/Fan-In ofrece una forma potente en Go para distribuir tareas entre goroutines y luego agregar los resultados. A diferencia del patrón Worker Pool, proporciona mayor flexibilidad mediante tuberías de procesamiento individuales para cada trabajador, lo que resulta especialmente útil para cadenas complejas de transformación de datos.

Principios fundamentales del patrón

El patrón se divide en dos fases clave. La fase Fan-Out distribuye los datos de entrada entre múltiples trabajadores (goroutines). Cada trabajador recibe su propia tarea, la procesa de forma independiente y envía el resultado a su canal dedicado. La fase Fan-In recopila luego los resultados de todos estos canales individuales en un único canal de salida para el procesamiento downstream.

La principal diferencia con el Worker Pool radica en la arquitectura de canales:

Google AdInline article slot
  • Worker Pool: Todos los trabajadores suelen leer tareas de un canal compartido y escriben resultados en otro canal compartido.
  • Fan-Out/Fan-In: Cada trabajador tiene su propio canal de salida, lo que requiere un paso adicional de fusión.

Este enfoque añade algo de sobrecarga debido a las goroutines y canales extra para la fusión, pero brilla en arquitecturas de tuberías donde las etapas posteriores necesitan flujos de datos separados.

Implementación práctica

Veamos una implementación práctica que se diferencia deliberadamente de un Worker Pool clásico para resaltar las características clave.

Función del trabajador

En esta configuración, el trabajador es una función que lanza una goroutine y devuelve un canal de resultados. Esto difiere de pasar un canal como parámetro.

Google AdInline article slot
func worker(x int, workerID int) chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        fmt.Printf("worker %d, job %d\n", workerID, x)
        out <- x * x
    }()
    return out
}

Detalles clave de implementación:

  • La goroutine cierra su canal out con defer después de enviar el resultado.
  • Para la demo, el trabajo es trivial: elevar un número al cuadrado.
  • En código de producción, refinarías esto: gestionar el ciclo de vida con contextos para apagados graciosos y timeouts.

Fase Fan-Out (Distribución)

El distribuidor (productor) lanza un trabajador por cada elemento de datos de entrada. Los canales devueltos por los trabajadores se almacenan en un slice para la fusión posterior.

for i, x := range data {
    chnls = append(chnls, worker(x, i+1))
}

Característica clave: El número de trabajadores escala dinámicamente con el conteo de tareas, lo que no es obligatorio pero muestra la flexibilidad del patrón. En apps reales, usualmente limitarías el número de trabajadores concurrentes.

Google AdInline article slot

Fase Fan-In y función de fusión

La función merge se encarga del trabajo crucial de combinar datos de múltiples canales en uno solo. Hace lo siguiente:

  • Crea un canal de salida único sin buffer out.
  • Por cada canal de entrada, lanza una goroutine que lee datos y los reenvía a out.
  • En una goroutine separada, espera a que todas las goroutines lectoras terminen usando sync.WaitGroup y luego cierra out.
func merge(chnls ...chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup

    for _, ch := range chnls {
        wg.Add(1)
        go func(c chan int) {
            defer wg.Done()
            for x := range c {
                out <- x
            }
        }(ch)
    }

    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

Nota técnica: La espera (wg.Wait()) se ejecuta en una goroutine separada. Hacerlo en el flujo principal podría causar un deadlock si el consumidor no ha empezado a leer de out, bloqueando las escrituras.

Consumidor

El consumidor lee resultados del canal único fusionado. El bucle for range sale automáticamente cuando merge cierra out.

for x := range out {
    results = append(results, x)
}

Lecciones clave

  • Flexibilidad arquitectónica: Tuberías individuales por trabajador facilitan insertar etapas extra de procesamiento de datos.
  • Escalado dinámico: Teóricamente soporta un trabajador por tarea, aunque en la práctica controlarías la concurrencia.
  • Sobrecarga: El patrón usa recursos extra (canales, goroutines) para la fusión en comparación con enfoques más simples.
  • Gestión del ciclo de vida: Esta demo omite contextos, válido para ejemplos pero esencial en producción para cancelaciones y timeouts.
  • Legibilidad del código: Canales sin buffer y bucles for range mantienen todo conciso y claro.

Recomendaciones para producción

Este código de demo es para aprender. Para uso real, realiza estas mejoras:

  • Añade contexto (context.Context): Pásalo a trabajadores y merge para manejar cancelaciones, plazos y timeouts.
  • Limita la concurrencia: Goroutines ilimitadas (una por tarea) pueden agotar recursos. Usa semáforos o un pool de trabajadores de tamaño fijo en Fan-Out.
  • Manejo de errores: Diseña para propagar errores de trabajadores al consumidor, como un canal de errores dedicado o struct de resultados.
  • Canales con buffer: Pueden mejorar el rendimiento reduciendo bloqueos en algunos casos, pero prueba con cuidado.

El patrón Fan-Out/Fan-In es una herramienta poderosa en el kit de todo desarrollador Go para construir tuberías de procesamiento de datos eficientes y escalables, especialmente cuando necesitas flexibilidad en la gestión de flujos de datos entre etapas.

— Editorial Team

Advertisement 728x90

Leer después