Оператор "Select" внутри goroutine с циклом for - PullRequest
0 голосов
/ 16 сентября 2018

Может кто-нибудь объяснить, почему, если в цикле есть бесконечный цикл for и select внутри цикла, код в цикле запускается только один раз?

package main

import (
    "time"
)

func f1(quit chan bool){
    go func() {
        for {
            println("f1 is working...")
            time.Sleep(1 * time.Second)

            select{
            case <-quit:
            println("stopping f1")
            break
            }
        }   
    }()
}

func main() {
    quit := make(chan bool)
    f1(quit)
    time.Sleep(4 * time.Second)
}

Выход:

f1 is working...
Program exited.

Но если «выбрать» закомментировано:

package main

import (
    "time"
)

func f1(quit chan bool){
    go func() {
        for {
            println("f1 is working...")
            time.Sleep(1 * time.Second)

            //select{
            //case <-quit:
            //println("stopping f1")
            //break
            //}
        }   
    }()
}

func main() {
    quit := make(chan bool)
    f1(quit)
    time.Sleep(4 * time.Second)
}

Выход:

f1 is working...
f1 is working...
f1 is working...
f1 is working...
f1 is working...

https://play.golang.org/p/MxKy2XqQlt8

1 Ответ

0 голосов
/ 16 сентября 2018

Оператор select без регистра default является блокирующим до тех пор, пока не будет выполнено чтение или запись хотя бы в одном из операторов case. Соответственно, ваш select будет блокироваться до тех пор, пока не будет возможно чтение с канала quit (либо значения, либо нулевого значения, если канал закрыт). Спецификация языка предоставляет конкретное описание этого поведения, в частности:

Если одно или несколько сообщений [выраженных в case заявлениях] могут продолжаться, то одно из них, которое может продолжаться, выбирается с помощью равномерного псевдослучайного выбора. В противном случае, если есть случай по умолчанию, этот случай выбирается. Если регистр по умолчанию отсутствует, оператор SELECT блокируется до тех пор, пока не будет продолжено хотя бы одно из сообщений.

Другие проблемы с кодом

Внимание! break относится к выражению select

Однако, даже если вы закрыли канал quit, чтобы сигнализировать об отключении вашей программы, ваша реализация, скорее всего, не даст желаемого эффекта. Вызов (без метки) break прекратит выполнение самого внутреннего for, select или switch оператора внутри функции. В этом случае оператор select прервется, и цикл for снова будет выполнен. Если quit был закрыт, он будет работать вечно, пока что-то еще не остановит программу, в противном случае он снова заблокируется в select ( Пример игровой площадки )

Закрытие quit (вероятно) не сразу остановит программу

Как отмечено в комментариях , вызов time.Sleep блокируется на секунду на каждой итерации цикла, поэтому любая попытка остановить программу закрытием quit будет отложена на срок до примерно за секунду до того, как программа проверяет quit и убегает. Вряд ли этот период ожидания должен завершиться полностью до остановки программы.

Более идиоматический Go будет блокировать в операторе select при получении из двух каналов:

  • Канал quit
  • Канал, возвращаемый time.After - этот вызов является абстракцией вокруг Timer, который спит в течение определенного периода времени, а затем записывает значение в предоставленный канал.

Разрешение

Решение с минимальными изменениями

Решение, чтобы исправить вашу непосредственную проблему с минимальными изменениями в вашем коде:

  • Сделайте чтение из quit неблокирующим, добавив регистр по умолчанию в оператор select.
  • Обеспечение выполнения процедуры на самом деле возвращается, когда чтение из quit завершается успешно:
    • Пометьте петлю for и используйте помеченный вызов break; или
    • return из функции f1, когда пришло время выйти (предпочтительно)

В зависимости от ваших обстоятельств, вам может показаться более идиоматичным Перейти к использованию context.Context для сигнализации о завершении и к использованию sync.WaitGroup для ожидания выполнения процедуры закончить перед возвращением с main.

package main

import (
    "fmt"
    "time"
)

func f1(quit chan bool) {
    go func() {
        for {
            println("f1 is working...")
            time.Sleep(1 * time.Second)

            select {
            case <-quit:
                fmt.Println("stopping")
                return
            default:
            }
        }
    }()
}

func main() {
    quit := make(chan bool)
    f1(quit)
    time.Sleep(4 * time.Second)
    close(quit)
    time.Sleep(4 * time.Second)
}

Рабочий пример

(Примечание: я добавил дополнительный вызов time.Sleep в ваш метод main, чтобы избежать его возврата сразу после вызова на close и завершения программы.)

Исправление проблемы блокировки сна

Чтобы устранить дополнительную проблему, связанную с блокировкой сна, предотвращающей немедленный выход, переместите сон в таймер в блоке select. Изменение вашего for цикла согласно этому примеру игровой площадки из комментариев делает именно это:

for {
    println("f1 is working...")

    select {
    case <-quit:
        println("stopping f1")
        return
    case <-time.After(1 * time.Second):
        // repeats loop
    }
}

Связанная литература

...