golang - ¿Cómo recopilar valores de N goroutines ejecutados en un orden específico?
learn go by example (2)
A continuación se muestra una estructura de tipo Stuff.
Tiene tres ints.
Un
Number
, su
Double
y su
Power
.
Supongamos que calcular el doble y la potencia de una lista dada de entradas es un cálculo costoso.
type Stuff struct {
Number int
Double int
Power int
}
func main() {
nums := []int{2, 3, 4} // given numbers
stuff := []Stuff{} // struct of stuff with transformed ints
double := make(chan int)
power := make(chan int)
for _, i := range nums {
go doubleNumber(i, double)
go powerNumber(i, power)
}
// How do I get the values back in the right order?
fmt.Println(stuff)
}
func doubleNumber(i int, c chan int) {
c <- i + i
}
func powerNumber(i int, c chan int) {
c <- i * i
}
El resultado de
fmt.Println(stuff)
debería ser el mismo que si se inicializaran cosas como:
stuff := []Stuff{
{Number: 2, Double: 4, Power: 4}
{Number: 3, Double: 6, Power: 9}
{Number: 4, Double: 8, Power: 16}
}
Sé que puedo usar
<- double
y
<- power
para recopilar valores de los canales, pero no sé qué double / powers pertenecen a qué números.
Las goroutines se ejecutan simultáneamente, de forma independiente, por lo que sin sincronización explícita no se puede predecir el orden de ejecución y finalización. Por lo tanto, no puede emparejar los números devueltos con los números de entrada.
Puede devolver más datos (por ejemplo, el número de entrada y la salida, envueltos en una estructura, por ejemplo), o pasar punteros a las funciones del trabajador (lanzadas como nuevas rutinas), por ejemplo
*Stuff
Rellenar y hacer que las rutinas llenen los datos calculados en el
Stuff
sí.
Devolviendo más datos
Usaré un canal tipo
chan Pair
donde
Pair
es:
type Pair struct{ Number, Result int }
Entonces el cálculo se verá así:
func doubleNumber(i int, c chan Pair) { c <- Pair{i, i + i} }
func powerNumber(i int, c chan Pair) { c <- Pair{i, i * i} }
Y usaré un
map[int]*Stuff
porque los datos recopilables provienen de múltiples canales (
double
y de
power
), y quiero encontrar el
Stuff
apropiado de manera fácil y rápida (se requiere un puntero para poder modificarlo "en el mapa" )
Entonces la función principal:
nums := []int{2, 3, 4} // given numbers
stuffs := map[int]*Stuff{}
double := make(chan Pair)
power := make(chan Pair)
for _, i := range nums {
go doubleNumber(i, double)
go powerNumber(i, power)
}
// How do I get the values back in the right order?
for i := 0; i < len(nums)*2; i++ {
getStuff := func(number int) *Stuff {
s := stuffs[number]
if s == nil {
s = &Stuff{Number: number}
stuffs[number] = s
}
return s
}
select {
case p := <-double:
getStuff(p.Number).Double = p.Result
case p := <-power:
getStuff(p.Number).Power = p.Result
}
}
for _, v := range nums {
fmt.Printf("%+v/n", stuffs[v])
}
Salida (pruébalo en Go Playground ):
&{Number:2 Double:4 Power:4}
&{Number:3 Double:6 Power:9}
&{Number:4 Double:8 Power:16}
Usando punteros
Como ahora estamos pasando los valores de
*Stuff
, podemos "rellenar previamente" el número de entrada en el
Stuff
mismo.
Pero se debe tener cuidado, solo puede leer / escribir valores con la sincronización adecuada. Lo más fácil es esperar a que todas las gorutinas "trabajadoras" terminen sus trabajos.
var wg = &sync.WaitGroup{}
func main() {
nums := []int{2, 3, 4} // given numbers
stuffs := make([]Stuff, len(nums))
for i, n := range nums {
stuffs[i].Number = n
wg.Add(2)
go doubleNumber(&stuffs[i])
go powerNumber(&stuffs[i])
}
wg.Wait()
fmt.Printf("%+v", stuffs)
}
func doubleNumber(s *Stuff) {
defer wg.Done()
s.Double = s.Number + s.Number
}
func powerNumber(s *Stuff) {
defer wg.Done()
s.Power = s.Number * s.Number
}
Salida (pruébalo en Go Playground ):
[{Number:2 Double:4 Power:4} {Number:3 Double:6 Power:9} {Number:4 Double:8 Power:16}]
Personalmente, usaría un
chan Stuff
para pasar los resultados nuevamente, luego giraría las rutinas de computación para calcular un
Stuff
completo y lo devolvería.
Si necesita varias partes de una sola
Stuff
calculada al mismo tiempo, puede generar goroutines de cada goroutine, utilizando canales dedicados.
Una vez que haya recopilado todos los resultados, puede (opcionalmente) ordenar la porción con los valores acumulados.
Ejemplo de lo que quiero decir a continuación (podría, en principio, usar una
sync.WaitGroup
para coordinar las cosas, pero si se conoce el recuento de entrada, no es estrictamente necesario).
type Stuff struct {
number int64
double int64
square int64
}
// Compute a Stuff with individual computations in-line, send it out
func computeStuff(n int64, out chan<- Stuff) {
rv := Stuff{number: n}
rv.double = n * 2
rv.square = n * n
out <- rv
}
// Compute a Stuff with individual computations concurrent
func computeStuffConcurrent(n int64, out chan<- Stuff) {
rv := Stuff{number: n}
dc := make(chan int64)
sc := make(chan int64)
defer close(dc)
defer close(sc)
go double(n, dc)
go square(n, sc)
rv.double = <-dc
rv.square = <-sc
out <- rv
}
func double(n int64, result chan<- int) {
result <- n * 2
}
func square(n int64, result chan<- int) {
result <- n * n
}
func main() {
inputs := []int64{1, 2, 3}
results := []Stuff{}
resultChannel := make(chan Stuff)
for _, input := range inputs {
go computeStuff(input, resultChannel)
// Or the concurrent version, if the extra performance is needed
}
for c := 0; c < len(inputs); c++ {
results = append(results, <- resultChannel)
}
// We now have all results, sort them if you need them sorted
}