Quiz: Evaluación de conceptos de concurrencia

Quiz
10 min~4 min lectura

Quiz Interactivo

Pon a prueba tus conocimientos

Concepto clave

La concurrencia en Go se basa en goroutines y channels. Una goroutine es una función que se ejecuta de manera independiente y ligera, similar a un hilo pero con menos sobrecarga. Los channels son tuberías que permiten la comunicación segura entre goroutines, evitando condiciones de carrera. Imagina una cocina de restaurante: cada cocinero es una goroutine preparando platos, y los canales son los carritos que transportan los pedidos entre estaciones sin que se choquen.

El modelo de concurrencia de Go sigue el principio "No comuniques compartiendo memoria; comparte memoria comunicándote". Esto significa que en lugar de usar locks y mutexes directamente, se prefieren channels para sincronizar goroutines. Para APIs de alto rendimiento, esto permite manejar miles de conexiones simultáneas eficientemente, como un servidor web que procesa múltiples solicitudes en paralelo sin bloquearse.

Cómo funciona en la práctica

En un microservicio REST con gorilla/mux, la concurrencia se aplica al manejar solicitudes HTTP. Cada request puede disparar goroutines para tareas como validación, acceso a bases de datos, o procesamiento de datos. Por ejemplo, al recibir una petición POST, puedes lanzar una goroutine para registrar logs mientras otra procesa el payload, mejorando el tiempo de respuesta.

Paso a paso: 1) Define un handler que use goroutines para operaciones paralelizables. 2) Usa channels para recoger resultados o errores. 3) Sincroniza con wait groups o select statements. 4) Maneja timeouts para evitar goroutines huérfanas. Esto asegura que tu API escale bien bajo carga alta, como en un sistema de e-commerce durante ventas flash.

Código en acción

Ejemplo de un handler concurrente que procesa múltiples solicitudes de datos:

package main

import (
    "fmt"
    "net/http"
    "sync"
    "time"
    "github.com/gorilla/mux"
)

func fetchData(id int, ch chan<- string, wg *sync.WaitGroup) {
    defer wg.Done()
    // Simula una operación lenta, como consultar una base de datos
    time.Sleep(100 * time.Millisecond)
    ch <- fmt.Sprintf("Datos para ID %d", id)
}

func concurrentHandler(w http.ResponseWriter, r *http.Request) {
    var wg sync.WaitGroup
    ch := make(chan string, 5) // Buffer para 5 resultados
    
    // Lanza goroutines para procesar 5 items concurrentemente
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go fetchData(i, ch, &wg)
    }
    
    // Espera a que todas las goroutines terminen
    wg.Wait()
    close(ch)
    
    // Recoge resultados
    for result := range ch {
        fmt.Fprintln(w, result)
    }
}

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/data", concurrentHandler)
    http.ListenAndServe(":8080", r)
}

Refactorización para agregar timeout:

func concurrentHandlerWithTimeout(w http.ResponseWriter, r *http.Request) {
    ch := make(chan string, 5)
    timeout := time.After(500 * time.Millisecond)
    
    for i := 1; i <= 5; i++ {
        go func(id int) {
            time.Sleep(100 * time.Millisecond)
            ch <- fmt.Sprintf("Datos para ID %d", id)
        }(i)
    }
    
    for i := 0; i < 5; i++ {
        select {
        case result := <-ch:
            fmt.Fprintln(w, result)
        case <-timeout:
            fmt.Fprintln(w, "Timeout alcanzado")
            return
        }
    }
}

Errores comunes

  • Fugas de goroutines: Lanzar goroutines sin control puede consumir memoria. Solución: Usar contextos con cancelación o timeouts para limitar su vida útil.
  • Deadlocks con channels: Enviar a un channel sin receptor, o viceversa, bloquea el programa. Solución: Asegurar que channels estén buffered o que haya goroutines escuchando.
  • Condiciones de carrera en datos compartidos: Acceder a variables desde múltiples goroutines sin sincronización causa resultados impredecibles. Solución: Usar channels o sync.Mutex para proteger el acceso.
  • Ignorar errores en goroutines: Los errores en goroutines pueden perderse si no se capturan. Solución: Pasar errores a un channel dedicado y manejarlos en el main.
  • Sobrecarga por demasiadas goroutines: Crear miles de goroutines para tareas triviales puede degradar el rendimiento. Solución: Usar worker pools o limitar la concurrencia con semáforos.

Checklist de dominio

  1. ¿Puedes explicar la diferencia entre goroutines y threads del sistema operativo?
  2. ¿Sabes implementar un handler HTTP que use goroutines para procesar solicitudes en paralelo?
  3. ¿Puedes usar channels para comunicar resultados entre goroutines de forma segura?
  4. ¿Manejas timeouts y cancelación con contextos para evitar goroutines huérfanas?
  5. ¿Identificas y evitas condiciones de carrera en código concurrente?
  6. ¿Utilizas wait groups o canales para sincronizar múltiples goroutines?
  7. ¿Optimizas el rendimiento ajustando el tamaño de buffers en channels o usando patrones como worker pools?

Optimizar un handler REST con concurrencia para procesamiento de imágenes

En este ejercicio, mejorarás un handler REST existente que procesa imágenes de manera secuencial, aplicando concurrencia para reducir el tiempo de respuesta. Sigue estos pasos:

  1. Clona o crea un proyecto Go con gorilla/mux que tenga un endpoint POST /process-images que reciba una lista de URLs de imágenes.
  2. Implementa una versión inicial secuencial que descargue y procese cada imagen (por ejemplo, redimensionándola) una por una, registrando el tiempo total.
  3. Refactoriza el handler para usar goroutines: lanza una goroutine por cada URL para descargar y procesar concurrentemente.
  4. Usa un channel para recoger los resultados procesados (por ejemplo, rutas de archivos o errores) y un wait group para sincronizar.
  5. Agrega un timeout de 10 segundos usando context.WithTimeout para cancelar las operaciones si tardan demasiado.
  6. Prueba con al menos 5 URLs simuladas (puedes usar placeholders) y compara los tiempos de la versión secuencial vs. concurrente.
  7. Documenta los cambios y asegúrate de manejar errores adecuadamente, como fallos en la descarga.
Pistas
  • Usa sync.WaitGroup para esperar a que todas las goroutines terminen antes de cerrar el channel.
  • Considera usar un channel buffered para evitar bloqueos si el número de imágenes es grande.
  • Para el timeout, explora el paquete context de Go y cómo integrarlo con HTTP handlers.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.