🐔WaitGroups Go
En Go, es común ejecutar varias goroutines en paralelo para aprovechar la concurrencia del lenguaje. Sin embargo, coordinar la finalización de estas goroutines puede ser un desafío. Aquí es donde los WaitGroups
resultan esenciales.
En esta sección aprenderé cómo esperar de manera eficiente a que un conjunto de goroutines terminen su ejecución antes de que el programa continúe mediante los WG
¿Qué es un WaitGroup?
Un WaitGroup
es un contador de goroutines que se utilizan para sincronizar la ejecución en Go. Con un WaitGroup
, puedes decirle al programa que espere a que un conjunto de goroutines termine su trabajo antes de continuar con la siguiente tarea. Este mecanismo es crucial para evitar que el programa termine antes de que las goroutines hayan completado su trabajo.
Uso Básico de WaitGroups
La funcionalidad de WaitGroup
se maneja principalmente a través de tres métodos:
Add(delta int)
: Incrementa el contador delWaitGroup
por el valor dedelta
.Done()
: Decrementa el contador delWaitGroup
en uno. Este método es típicamente llamado por las goroutines cuando terminan su trabajo.Wait()
: Bloquea la ejecución hasta que el contador delWaitGroup
llegue a cero.
Ejemplo Básico: Sincronización de Goroutines
package main
import (
"fmt"
"sync"
)
func tarea(id int, wg *sync.WaitGroup) {
defer wg.Done() // Asegura que Done() se llame cuando la función termine
fmt.Printf("Goroutine %d está trabajando...\\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // Incrementa el contador del WaitGroup
go tarea(i, &wg) //&w referencia del objeto, para no trabajar con copias
}
wg.Wait() // Espera hasta que todas las goroutines hayan terminado
fmt.Println("Todas las goroutines han terminado")
}
Explicación del Código
Declaración del
WaitGroup
: Se declara unWaitGroup
(var wg sync.WaitGroup
) para controlar la sincronización de las goroutines.Incremento del Contador con
Add
: Dentro del bucle,wg.Add(1)
incrementa el contador delWaitGroup
cada vez que se lanza una nueva goroutine.Decremento del Contador con
Done
: Dentro de cada goroutine,defer wg.Done()
asegura que el contador se decremente cuando la goroutine termine.Espera a la Finalización con
Wait
: Finalmente,wg.Wait()
bloquea la ejecución hasta que todas las goroutines hayan llamado aDone()
y el contador llegue a cero.Con &wg paso una referencia al objeto Wg: ya que al usar
&wg
, estoy pasando la dirección de memoria del objetowg
, lo que permite que las goroutines accedan y modifiquen el mismoWaitGroup
en lugar de trabajar con una copia.
Salida esperada:
Goroutine 1 está trabajando...
Goroutine 2 está trabajando...
Goroutine 3 está trabajando...
Goroutine 4 está trabajando...
Goroutine 5 está trabajando...
Todas las goroutines han terminado
Asegurando el Uso Correcto de WaitGroups
Uso de Add
antes de Lanzar la Goroutine
Add
antes de Lanzar la GoroutineEs importante siempre llamar a wg.Add(1)
antes de lanzar la goroutine. Si Add
se llama después, existe la posibilidad de que Wait
termine antes de que la goroutine se registre en el WaitGroup
, lo que puede llevar a un comportamiento inesperado.
Ejemplo Incorrecto:
package main
import (
"fmt"
"sync"
"time"
)
func tarea(id int, wg *sync.WaitGroup) {
defer wg.Done()
time.Sleep(1 * time.Second)
fmt.Printf("Goroutine %d completada.\\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
go tarea(i, &wg) // ¡Error! `wg.Add(1)` debería ir antes de lanzar la goroutine
wg.Add(1)
}
wg.Wait()
fmt.Println("Todas las goroutines han terminado")
}
Este código puede resultar en un comportamiento inesperado porque wg.Wait()
podría ejecutarse antes de que todas las goroutines llamen a Add(1)
.
Evitar los Panics por Llamadas Incorrectas
Es crucial que el número de llamadas a wg.Done()
coincida con el número de llamadas a wg.Add()
. De lo contrario, puedes encontrarte con un panic
si el contador de WaitGroup
se vuelve negativo.
Ejemplo de Error Común:
package main
import (
"fmt"
"sync"
)
func tarea(id int, wg *sync.WaitGroup) {
fmt.Printf("Goroutine %d está trabajando...\\n", id)
// ¡Olvidamos llamar a wg.Done() aquí!
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go tarea(i, &wg)
}
wg.Wait() // Esto podría bloquearse indefinidamente
fmt.Println("Todas las goroutines han terminado")
}
En este ejemplo, el programa se bloqueará en wg.Wait()
porque no se llama a wg.Done()
en la goroutine, lo que impide que el contador llegue a cero.
Ejemplo Completo: Sincronización de Múltiples Tareas Concurrentes
Para mostrar el poder de los WaitGroups
, consideremos un ejemplo donde diferentes goroutines realizan diferentes tareas de duración variable.
//Author: @JFOZ1010 -> Github
package main
import (
"fmt"
"sync"
"time"
)
func tareaRapida(wg *sync.WaitGroup) {
defer wg.Done() //para decrementar cada que cada go routine termine.justo antes de finalizar la funcion con defer.
time.Sleep(1 * time.Second) //un tiempo de prueba de 1 segundo.
fmt.Println("Tarea rápida completada.")
}
func tareaLenta(wg *sync.WaitGroup) {
defer wg.Done()
time.Sleep(3 * time.Second)
fmt.Println("Tarea lenta completada.")
}
func main() {
var wg sync.WaitGroup
wg.Add(1) //incrementamos el contador del wg con cada go routine que se va a ejecutar, en este caso la llamamos en su linea sucesiva con un go routine function.
go tareaRapida(&wg) //aqui con el anpersand estamos haciendo una referencia al objeto con el fin de acceder a cada rutina en su memoria, para cada go routine.
wg.Add(1) //incrementamos el contador del wg con cada go routine que se va a ejecutar, en este caso la llamamos en su linea sucesiva con un go routine function.
go tareaLenta(&wg) //aqui con el anpersand estamos haciendo una referencia al objeto con el fin de acceder a cada rutina en su memoria, para cada go routine.
wg.Wait()
fmt.Println("Todas las tareas han terminado.")
}
Explicación del Código
Función
tareaRapida
: Simula una tarea que toma 1 segundo en completarse.Función
tareaLenta
: Simula una tarea que toma 3 segundos en completarse.Sincronización: Ambas tareas se ejecutan en paralelo, y
wg.Wait()
asegura que el programa no termine hasta que ambas goroutines completen su trabajo.
Salida esperada:
Tarea rápida completada.
Tarea lenta completada.
Todas las tareas han terminado.
Conclusión
Los WaitGroups
son una herramienta esencial en Go para manejar la sincronización de goroutines de manera eficiente. Aseguran que un conjunto de tareas concurrentes termine antes de proceder con la siguiente parte del programa, lo que es crucial para evitar problemas comunes en programación concurrente como condiciones de carrera o bloqueos inesperados.
Usando Add
, Done
y Wait
correctamente, puedes escribir código concurrente robusto y fácil de mantener.
Last updated