Можно ли динамически изменять счетчик `sync.WaitGroup`?

Ответ

Да, счетчик sync.WaitGroup можно изменять динамически, но делать это нужно с осторожностью, чтобы избежать паники или состояний гонки.

Основные методы для этого — Add(delta int) для увеличения (или уменьшения) счетчика и Done() для его уменьшения на единицу.

Пример динамического добавления задач:

package main

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

func main() {
    var wg sync.WaitGroup

    // Запускаем 3 начальные горутины
    wg.Add(3)
    for i := 1; i <= 3; i++ {
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Задача %d выполняетсяn", id)
            time.Sleep(100 * time.Millisecond)
        }(i)
    }

    // В процессе выполнения решаем добавить еще 2 задачи
    fmt.Println("Динамически добавляем еще 2 задачи...")
    wg.Add(2)
    for i := 4; i <= 5; i++ {
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Дополнительная задача %d выполняетсяn", id)
            time.Sleep(200 * time.Millisecond)
        }(i)
    }

    wg.Wait() // Ожидаем завершения всех 5 задач
    fmt.Println("Все задачи выполнены.")
}

Важные правила и риски:

  1. Состояние гонки (Race Condition): Вызывать Add() нужно до того, как счетчик может стать равен нулю. Если wg.Wait() сработает до того, как другая горутина вызовет wg.Add(), программа завершится преждевременно. Лучшая практика — вызывать Add() в той же горутине, которая запускает новые дочерние горутины, и делать это до их запуска.
  2. Паника при отрицательном счетчике: Если вызов Done() или Add() с отрицательным значением сделает внутренний счетчик отрицательным, программа запаникует (panic: sync: negative WaitGroup counter).
  3. Паника после Wait(): Повторное использование WaitGroup после того, как Wait() уже отработал (т.е. счетчик стал 0), небезопасно. Если нужно повторить цикл ожидания, создайте новый экземпляр WaitGroup.