Подтвердить что ты не робот

Генераторы в стиле Python в Go

В настоящее время я работаю над Tour of Go, и я думал, что goroutines были использованы аналогично генераторам Python, особенно с Вопрос 66. Я думал, что 66 выглядит сложным, поэтому я переписал его так:

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)
    }
}

Это похоже на работу. Несколько вопросов:

  • Если я укажу размер буфера на канале, чтобы сказать, 10, fibonacci заполнит еще 10 точек, как можно быстрее, и main будет съедать пятна так быстро, как только может. Это правильно? Это будет более результативным, чем размер буфера 1 за счет памяти, правильно?
  • Поскольку канал не закрывается отправителем fibonacci, что происходит с памятью, когда мы выходим из сферы действия здесь? Мое ожидание состоит в том, что когда c и go fibonacci выходит за пределы области видимости, канал и все на нем получают сбор мусора. Моя кишка говорит мне, что это, вероятно, не то, что происходит.
4b9b3361

Ответ 1

Мне нравится @tux21b answer; наличие канала, созданного в функции fib(), делает код вызова приятным и чистым. Чтобы разработать немного, вам нужен только отдельный канал "выйти", если нет возможности сообщить функции, когда останавливаться при ее вызове. Если вы только заботитесь о "числах до X", вы можете сделать это:

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)
    }
}

Если вам нужна возможность сделать это, это немного небрежно, но мне лично это нравится лучше, чем тестирование условия в вызывающем, а затем сигнализация о выходе из отдельного канала:

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)
    }
}

Я думаю, что это зависит только от особенностей данной ситуации:

  • Сообщите генератору, когда нужно остановиться, когда вы его создаете
    • Передача явного количества значений для генерации
    • Передача значения цели
    • Передача функции, которая определяет, продолжать ли
  • Дайте генератору канал "quit", проверьте значения самостоятельно и сообщите ему, чтобы он ушел, когда это необходимо.

Завершить и ответить на ваши вопросы:

  • Увеличение размера канала поможет повысить производительность из-за меньшего количества переключателей контекста. В этом тривиальном примере ни производительность, ни потребление памяти не будут проблемой, но в других ситуациях буферизация канала часто является очень хорошей идеей. В большинстве случаев память, используемая make (chan int, 100), вряд ли кажется значимой, но она может легко сделать большую разницу в производительности.

  • У вас есть бесконечный цикл в вашей функции fibonacci, так что запущенный в него goroutine будет запускаться (блокировать на c <- x, в данном случае) навсегда. Тот факт, что (однажды c выходит за пределы области действия в вызывающем абоненте), вы больше никогда не будете читать из канала, который вы делитесь с ним, и не меняет этого. И, как отметил @tux21b, канал никогда не будет собираться с мусором, поскольку он все еще используется. Это не имеет никакого отношения к закрытию канала (цель которого - позволить получающему концу канала знать, что больше не будет больше значений) и все, что нужно сделать, чтобы не возвращаться из вашей функции.

Ответ 2

Да, увеличение размера буфера может значительно увеличить скорость выполнения вашей программы, поскольку это уменьшит количество переключателей контекста. Горутины не собираются с мусором, но каналы есть. В вашем примере, fibonacci goroutine будет работать вечно (ожидая, что еще один goroutine будет читать из канала c), и канал c никогда не будет уничтожен, потому что фибо-горутин все еще использует его.

Вот еще одна, по-видимому, различная программа, которой не хватает памяти и она больше похожа на генераторы 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)
    }
}

В качестве альтернативы, если вы не знаете, сколько чисел Фибоначчи вы хотите сгенерировать, вам нужно использовать другой канал выхода, чтобы вы могли отправить генератор goroutine сигнал, когда он должен остановиться. Это объясняется в учебнике golang https://tour.golang.org/concurrency/4.

Ответ 3

Вы можете использовать замыкания для имитации генератора. Вот пример из 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())
}

Ответ 4

Использование каналов для эмуляции генераторов Python - это работы, но они вводят concurrency, где нет необходимости, и это добавляет больше осложнений, чем это необходимо. Здесь просто держать государство явно легче, короче и почти наверняка более эффективно. Он задает все ваши вопросы о размерах буферов и сборе мусора.

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())
    }
}