Для чего используется `sync.WaitGroup` и каковы правила работы с ним?

Ответ

sync.WaitGroup — это механизм синхронизации, который позволяет основной горутине ожидать завершения выполнения набора других (дочерних) горутин.

Основные методы:


  1. Add(delta int)

    Увеличивает внутренний счетчик активных горутин на delta. Важно вызывать этот метод в основной горутине до запуска дочерней горутины, чтобы избежать состояния гонки.



  2. Done()

    Уменьшает счетчик на единицу. Обычно вызывается в конце работы горутины с помощью defer, чтобы гарантировать выполнение даже в случае паники внутри горутины.



  3. Wait()

    Блокирует горутину, в которой был вызван, до тех пор, пока внутренний счетчик не станет равен нулю.


Пример использования:

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Уменьшаем счетчик при выходе из функции

    fmt.Printf("Worker %d startingn", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d donen", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1) // Увеличиваем счетчик перед запуском горутины
        go worker(i, &wg)
    }

    fmt.Println("Main: Waiting for workers to finish...")
    wg.Wait() // Ожидаем, пока счетчик не станет 0
    fmt.Println("Main: All workers finished. Exiting.")
}

Ключевые правила и частые ошибки:

  • Вызов Add до запуска горутины: Если вызвать wg.Add(1) внутри самой горутины, wg.Wait() может сработать раньше, чем Add выполнится, что приведет к преждевременному завершению программы.
  • Отрицательный счетчик: Количество вызовов Done() не должно превышать количество, указанное в Add. Это вызовет панику panic: sync: negative WaitGroup counter.
  • Копирование WaitGroup: WaitGroup нельзя копировать после первого использования. Ее следует передавать по указателю (*sync.WaitGroup).
  • Переиспользование: После того как Wait() отработал и счетчик обнулился, WaitGroup можно использовать заново для новой группы горутин, снова вызвав Add.