golang - ¿Cuál es la ventaja de sync.WaitGroup sobre los canales?
golang concurrency (5)
Estoy trabajando en una biblioteca Go concurrente, y encontré dos patrones distintos de sincronización entre goroutines cuyos resultados son similares:
Usando Waitgroup
var wg sync.WaitGroup
func main() {
words := []string{ "foo", "bar", "baz" }
for _, word := range words {
wg.Add(1)
go func(word string) {
time.Sleep(1 * time.Second)
defer wg.Done()
fmt.Println(word)
}(word)
}
// do concurrent things here
// blocks/waits for waitgroup
wg.Wait()
}
Usando el canal
func main() {
words = []string{ "foo", "bar", "baz" }
done := make(chan bool)
defer close(done)
for _, word := range words {
go func(word string) {
time.Sleep(1 * time.Second)
fmt.Println(word)
done <- true
}(word)
}
// Do concurrent things here
// This blocks and waits for signal from channel
<-done
}
Me informaron que sync.WaitGroup
es un poco más sync.WaitGroup
, y he visto que se usa comúnmente. Sin embargo, encuentro canales más idiomáticos. ¿Cuál es la ventaja real de usar sync.WaitGroup
en los canales y / o cuál podría ser la situación cuando sea mejor?
A menudo utilizo canales para recopilar mensajes de error de goroutines que podrían producir un error. Aquí hay un ejemplo simple:
func couldGoWrong() (err error) {
errorChannel := make(chan error, 3)
// start a go routine
go func() (err error) {
defer func() { errorChannel <- err }()
for c := 0; c < 10; c++ {
_, err = fmt.Println(c)
if err != nil {
return
}
}
return
}()
// start another go routine
go func() (err error) {
defer func() { errorChannel <- err }()
for c := 10; c < 100; c++ {
_, err = fmt.Println(c)
if err != nil {
return
}
}
return
}()
// start yet another go routine
go func() (err error) {
defer func() { errorChannel <- err }()
for c := 100; c < 1000; c++ {
_, err = fmt.Println(c)
if err != nil {
return
}
}
return
}()
// synchronize go routines and collect errors here
for c := 0; c < cap(errorChannel); c++ {
err = <-errorChannel
if err != nil {
return
}
}
return
}
Depende del caso de uso. Si está enviando trabajos WaitGroup
para que se ejecuten en paralelo sin necesidad de conocer los resultados de cada trabajo, entonces puede usar un WaitGroup
. Pero si necesita recopilar los resultados de los goroutines, entonces debe usar un canal.
Como un canal funciona en ambos sentidos, casi siempre uso un canal.
En otra nota, como se señaló en el comentario, su ejemplo de canal no está implementado correctamente. Necesitaría un canal separado para indicar que no hay más trabajos por hacer ( here hay un ejemplo). En su caso, ya que conoce el número de palabras de antemano, solo puede usar un canal con búfer y recibir un número fijo de veces para evitar declarar un canal cercano.
Independientemente de la corrección de su segundo ejemplo (como se explica en los comentarios, no está haciendo lo que piensa, pero es fácil de arreglar), tiendo a pensar que el primer ejemplo es más fácil de entender.
Ahora, ni siquiera diría que los canales son más idiomáticos. Los canales que son una característica de la firma del lenguaje Go no deben significar que es idiomático utilizarlos siempre que sea posible. Lo que es idiomático en Go es usar la solución más sencilla y fácil de entender: aquí, el WaitGroup
transmite tanto el significado (su función principal es Wait
los trabajadores estén listos) y el mecánico (los trabajadores notifican cuando Done
).
A menos que esté en un caso muy específico, no recomiendo usar la solución de canal aquí.
Si está particularmente concentrado en usar solo canales, entonces debe hacerse de manera diferente (si usamos su ejemplo, como lo señala @Not_a_Golfer, producirá resultados incorrectos).
Una forma es hacer un canal de tipo int. En el proceso de trabajo, envíe un número cada vez que complete el trabajo (esta también puede ser la única identificación del trabajo, si lo desea, puede rastrearlo en el receptor).
En la rutina de inicio del receptor (que conocerá el número exacto de trabajos enviados): realice un ciclo de rango en un canal, cuente hasta que no se complete el número de trabajos enviados y salga del ciclo cuando se completen todos los trabajos. Esta es una buena manera si desea realizar un seguimiento de cada uno de los trabajos completados (y quizás hacer algo si es necesario).
Aquí está el código para su referencia. ¡La reducción de totalJobsLeft será segura ya que se hará solo en el rango del bucle del canal!
//This is just an illustration of how to sync completion of multiple jobs using a channel
//A better way many a times might be to use wait groups
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
comChannel := make(chan int)
words := []string{"foo", "bar", "baz"}
totalJobsLeft := len(words)
//We know how many jobs are being sent
for j, word := range words {
jobId := j + 1
go func(word string, jobId int) {
fmt.Println("Job ID:", jobId, "Word:", word)
//Do some work here, maybe call functions that you need
//For emulating this - Sleep for a random time upto 5 seconds
randInt := rand.Intn(5)
//fmt.Println("Got random number", randInt)
time.Sleep(time.Duration(randInt) * time.Second)
comChannel <- jobId
}(word, jobId)
}
for j := range comChannel {
fmt.Println("Got job ID", j)
totalJobsLeft--
fmt.Println("Total jobs left", totalJobsLeft)
if totalJobsLeft == 0 {
break
}
}
fmt.Println("Closing communication channel. All jobs completed!")
close(comChannel)
}
También sugiera utilizar el grupo de espera, pero aún así desea hacerlo con el canal, a continuación menciono un uso simple del canal.
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan string)
words := []string{"foo", "bar", "baz"}
go printWordrs(words, c)
for j := range c {
fmt.Println(j)
}
}
func printWordrs(words []string, c chan string) {
defer close(c)
for _, word := range words {
time.Sleep(1 * time.Second)
c <- word
}
}