sync.WaitGroup es un primitivo de sincronización que mantiene un contador interno para controlar un grupo de operaciones concurrentes. Su propósito es permitir que una goroutine (normalmente la principal) bloquee su ejecución hasta que todas las goroutines que dependen de ella hayan terminado su tarea. Internamente, utiliza operaciones atómicas para incrementar y decrementar este contador de forma segura entre múltiples hilos de ejecución. Debes usarlo cuando necesites coordinar el cierre de múltiples procesos paralelos, como cuando lanzas varios workers para procesar un lote de archivos y necesitas asegurarte de que todos terminen antes de realizar una acción de limpieza o consolidación de resultados. Si gestionas mal el contador, el programa puede terminar antes de que las tareas completen su trabajo (causando una finalización prematura) o lanzar un panic si intentas decrementar el contador cuando este ya es cero.
package main
import (
"fmt"
"sync"
"time"
)
// procesarTarea simula una carga de trabajo asíncrona.
// Es fundamental pasar el WaitGroup por puntero para que todas
// las goroutines operen sobre la misma estructura de datos.
func procesarTarea(id int, wg *sync.WaitGroup) {
// defer garantiza que Done() se ejecute incluso si la función
// termina por un error o un panic, evitando deadlocks.
defer wg.Done()
fmt.Printf("Tarea %d: Iniciando procesamiento...\n", id)
// Simulamos una operación de I/O o cálculo intensivo
time.Sleep(time.Duration(id) * time.Second)
fmt.Printf("Tarea %d: Finalizada con éxito.\n", id)
}
func main() {
var wg sync.WaitGroup
numerosDeTareas := []int{1, 2, 3, 4, 5}
for _, n := range numerosDeTareas {
// Incrementamos el contador ANTES de lanzar la goroutine.
// Esto garantiza que Wait() vea el contador antes de que
// las goroutines empiecen a ejecutarse.
wg.Add(1)
go procesarTarea(n, &wg)
}
fmt.Println("Main: Esperando la finalización de todas las tareas...")
// Wait bloquea la ejecución de main hasta que el contador llegue a 0.
wg.Wait()
fmt.Println("Main: Todas las tareas terminaron. Procediendo con el cierre.")
}
En el ejemplo anterior, wg.Wait() actúa como una barrera de sincronización. El programa principal se detiene en esa línea y no continuará hasta que el contador interno de wg sea exactamente cero.
Fíjate en wg.Add(1) dentro del bucle for. Esta llamada incrementa el contador en 1 antes de que la palabra clave go lance la nueva goroutine. Es crucial que esto ocurra en la goroutine “padre” y no dentro de la función que se lanza, para evitar condiciones de carrera. Si la función procesarTarea tardara un milisegundo en iniciar, el hilo principal podría llegar a wg.Wait() antes de que la goroutine haya tenido oportunidad de ejecutar su propio Add(1), viendo un contador de 0 y terminando el programa antes de tiempo.
Dentro de procesarTarea, utilizamos defer wg.Done(). Como Done es simplemente un alias de Add(-1), decrementa el contador. Usar defer es una buena práctica de ingeniería para asegurar que, sin importar la ruta de salida de la función, el contador siempre se decremente, evitando que el programa principal se quede bloqueado para siempre en un deadlock. Además, observa que la función recibe wg *sync.WaitGroup. En Go, si pasas un WaitGroup por valor, estás copiando la estructura de datos, incluyendo su estado interno; esto haría que la goroutine trabaje sobre una copia distinta, y el Wait() original nunca recibiría la señal de terminación.
El error frecuente
El error más sutil y peligroso con sync.WaitGroup ocurre cuando intentas incrementar el contador dentro de la goroutine lanzada.
// MAL: Esto causará una race condition
for i := 0; i < 5; i++ {
go func(n int) {
wg.Add(1) // ERROR: El contador puede ser 0 cuando Wait() se ejecute
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait()
En este escenario, existe una ventana de tiempo crítica entre el lanzamiento de la goroutine y la ejecución de wg.Add(1). Si el planificador de Go decide que el hilo principal debe seguir ejecutándose inmediatamente, wg.Wait() verá un contador de cero y finalizará el programa antes de que cualquier goroutine haya alcanzado la línea wg.Add(1). El resultado es un programa que parece funcionar a veces, pero falla aleatoriamente en producción bajo carga.
N° 144