Управление Goroutine

Горутины легковесны, но они не бесплатны: в крайнем случае они потребляют память для стека и планирования ЦП. Хотя эти затраты незначительны для использования горутин, их большое количество без контролируемого жизненного цикла может привести к серьезным проблемам производительности. Горутины с неуправляемыми жизненными циклами также могут привести к другим проблемам, таким как предотвращение сборки мусора для неиспользуемых объектов и удержание неиспользуемых ресурсов.

Поэтому не допускайте утечек горутин в своем коде. Используйте go.uber.org/goleak для тестирования утечек горутин в пакете, который может создавать горутины.

В целом каждая горутина должна:

  • Иметь предсказуемое время остановки; или
  • Иметь способ сигнализировать горутине, что она должна остановиться.

В обоих случаях должен быть способ для блокировки кода и ожидания завершения горутины.

Например:

Не рекомендуется:

go func() {
  for {
    flush()
    time.Sleep(delay)
  }
}()
// Нет способа остановить эту горутину. Она будет работать бесконечно, пока приложение не завершится.

Рекомендуется:

var (
  stop = make(chan struct{}) // Сигнализирует горутине остановиться
  done = make(chan struct{}) // Сигнал о завершении горутины
)
go func() {
  defer close(done)
  ticker := time.NewTicker(delay)
  defer ticker.Stop()
  for {
    select {
    case <-ticker.C:
      flush()
    case <-stop:
      return
    }
  }
}()

// ... другой код
close(stop)  // Указывает, что горутина должна остановиться
<-done       // и ждет ее завершения

// На эту горутину можно ожидать с помощью close(stop), и мы можем ожидать ее завершения <-done.

Ожидание завершения горутин

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

  • Использование sync.WaitGroup. Используйте этот метод, когда ожидаете завершения нескольких горутин.
var wg sync.WaitGroup
for i := 0; i < num; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        // ...
    }()
}
wg.Wait()
  • Добавление еще одного chan struct{} и его закрытие после завершения горутины. Используйте этот метод, если есть только одна горутина.
done := make(chan struct{})
go func() {
    defer close(done)
    // ...
}()
// Чтобы ожидать завершения горутины:
<-done

Функция init() не должна создавать горутины. Также обратите внимание на Избегайте использования init().

Если пакету требуется фоновая горутина, он должен предоставить объект, ответственный за управление жизненным циклом горутины. Этот объект должен предоставить метод (Close, Stop, Shutdown и т. д.) для указания завершения фоновой горутины и ожидания ее завершения.

Не рекомендуется подход:

func init() {
    go doWork()
}
func doWork() {
    for {
        // ...
    }
}
// Когда пользователь экспортирует этот пакет, автоматически создается фоновая горутина. Пользователи не могут управлять горутиной или остановить ее.

Рекомендуемый подход:

type Worker struct{ /* ... */ }
func NewWorker(...) *Worker {
    w := &Worker{
        stop: make(chan struct{}),
        done: make(chan struct{}),
        // ...
    }
    go w.doWork()
}
func (w *Worker) doWork() {
    defer close(w.done)
    for {
        // ...
        select {
        case <-w.stop:
            return
        }
    }
}
// Shutdown сообщает работнику остановиться
// и ждет его завершения.
func (w *Worker) Shutdown() {
    close(w.stop)
    <-w.done
}

Генерируйте рабочего только по запросу пользователя и предоставьте метод для завершения работы рабочего, чтобы пользователь мог освободить используемые ресурсы.

Обратите внимание, что если рабочий управляет несколькими горутинами, следует использовать WaitGroup.