python go generator goroutine

python - Generadores estilo pitón en Go



generator goroutine (4)

El uso de canales para emular el tipo de trabajo de los generadores de Python, pero introducen la concurrencia donde no se necesita, y agrega más complicaciones de las que probablemente sea necesaria. Aquí, mantener el estado explícitamente es más fácil de entender, más breve y, casi con toda seguridad, más eficiente. Hace que todas sus preguntas sobre tamaños de búfer y recolección de basura sean discutibles.

type fibState struct { x, y int } func (f *fibState) Pop() int { result := f.x f.x, f.y = f.y, f.x + f.y return result } func main() { fs := &fibState{1, 1} for i := 0; i < 10; i++ { fmt.Println(fs.Pop()) } }

Actualmente estoy trabajando en el Tour of Go , y pensé que los goroutines se han usado de manera similar a los generadores de Python, particularmente con la Pregunta 66 . Pensé que 66 parecía complejo, así que lo reescribí a esto:

package main import "fmt" func fibonacci(c chan int) { x, y := 1, 1 for { c <- x x, y = y, x + y } } func main() { c := make(chan int) go fibonacci(c) for i := 0; i < 10; i++ { fmt.Println(<-c) } }

Esto parece funcionar. Un par de preguntas:

  1. Si subo el tamaño del búfer en el canal para decir, 10, fibonacci llenaría 10 puntos más, lo más rápido posible, y main se comería los puntos lo más rápido posible. ¿Es esto correcto? Esto sería más eficaz que un tamaño de búfer de 1 a expensas de la memoria, ¿correcto?
  2. Como el remitente de fibonacci no cierra el canal, ¿qué sucede con la memoria cuando estamos fuera del alcance aquí? Mi expectativa es que una vez que c e go fibonacci esté fuera de alcance, el canal y todo lo que hay en él se recolecta de basura. Mi instinto me dice que esto probablemente no es lo que sucede.

Me gusta la respuesta de @ tux21b; tener el canal creado en la función fib() hace que el código de llamada sea agradable y limpio. Para elaborar un poco, solo necesita un canal separado para "dejar de fumar" si no hay manera de decirle a la función cuándo parar cuando lo llama. Si solo te interesan los "números hasta X", puedes hacer esto:

package main import "fmt" func fib(n int) chan int { c := make(chan int) go func() { x, y := 0, 1 for x < n { c <- x x, y = y, x+y } close(c) }() return c } func main() { // Print the Fibonacci numbers less than 500 for i := range fib(500) { fmt.Println(i) } }

Si desea poder hacer cualquiera de las dos cosas, esto es un poco descuidado, pero personalmente me gusta más que probar la condición en la persona que llama y luego indicar una renuncia a través de un canal separado:

func fib(wanted func (int, int) bool) chan int { c := make(chan int) go func() { x, y := 0, 1 for i := 0; wanted(i, x); i++{ c <- x x, y = y, x+y } close(c) }() return c } func main() { // Print the first 10 Fibonacci numbers for n := range fib(func(i, x int) bool { return i < 10 }) { fmt.Println(n) } // Print the Fibonacci numbers less than 500 for n := range fib(func(i, x int) bool { return x < 500 }) { fmt.Println(n) } }

Creo que solo depende de los detalles de una situación dada si:

  1. Dile al generador cuándo parar cuando lo creas
    1. Pasando un número explícito de valores a generar.
    2. Pasando un valor objetivo
    3. Pasando una función que determina si seguir adelante.
  2. Déle al generador un canal de ''dejar de fumar'', pruebe los valores usted mismo y dígale que salga cuando sea apropiado.

Para terminar y realmente responder a sus preguntas:

  1. Aumentar el tamaño del canal ayudaría al rendimiento debido a menos cambios de contexto. En este ejemplo trivial, ni el rendimiento ni el consumo de memoria serán un problema, pero en otras situaciones, el almacenamiento en búfer del canal suele ser una muy buena idea. La memoria utilizada por make (chan int, 100) casi no parece significativa en la mayoría de los casos, pero podría fácilmente marcar una gran diferencia de rendimiento.

  2. Tiene un bucle infinito en su función de fibonacci , por lo que el goroutine que se ejecuta se ejecutará (bloque en c <- x , en este caso) para siempre. El hecho de que (una vez que c quede fuera del alcance en la persona que llama) no volverá a leer el canal con el que comparte, no cambia eso. Y como señaló @ tux21b, el canal nunca será recolectado basura ya que todavía está en uso. Esto no tiene nada que ver con cerrar el canal (cuyo propósito es permitir que el extremo receptor del canal sepa que no vendrán más valores) y todo lo relacionado con no regresar de su función.


Podrías usar cierres para simular un generador. Aquí está el ejemplo de golang.org .

package main import "fmt" // fib returns a function that returns // successive Fibonacci numbers. func fib() func() int { a, b := 0, 1 return func() int { a, b = b, a+b return a } } func main() { f := fib() // Function calls are evaluated left-to-right. fmt.Println(f(), f(), f(), f(), f()) }


Sí, aumentar el tamaño del búfer puede aumentar drásticamente la velocidad de ejecución de su programa, ya que reducirá el número de cambios de contexto. Los goroutines no son recolectados en basura, pero los canales sí. En su ejemplo, la fibonacci goroutine se ejecutará para siempre (a la espera de que otra goroutine lea el canal c), y el canal c nunca se destruirá, porque la fib-goroutine todavía la está utilizando.

Aquí hay otro programa, muy diferente, que no carece de memoria y es más similar a los generadores de Python:

package main import "fmt" func fib(n int) chan int { c := make(chan int) go func() { x, y := 0, 1 for i := 0; i <= n; i++ { c <- x x, y = y, x+y } close(c) }() return c } func main() { for i := range fib(10) { fmt.Println(i) } }

Alternativamente, si no sabe cuántos números de Fibonacci desea generar, tiene que usar otro canal de salida para poder enviar una señal al generador de goroutine cuando deba detenerse. Esto es lo que se explica en el tutorial de golang https://tour.golang.org/concurrency/4 .