← Назад к вопросам

Можно ли изменять Wait Group динамически?

2.0 Middle🔥 122 комментариев
#Другое

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Можно ли динамически изменять WaitGroup в Go?

Нет, стандартный sync.WaitGroup из пакета sync не предназначен для динамического изменения его начального счётчика после начала использования. Это принципиальное архитектурное решение, обеспечивающее безопасность и предсказуемость конкурентных операций.

Принцип работы WaitGroup

WaitGroup — это простой и эффективный примитив синхронизации, основанный на счётчике. Его API состоит из трёх ключевых методов:

  • wg.Add(delta int) — увеличивает (при delta > 0) или уменьшает (при delta < 0) внутренний счётчик.
  • wg.Done() — уменьшает счётчик на 1 (эквивалентно wg.Add(-1)).
  • wg.Wait() — блокирует вызывающую горутину до тех пор, пока счётчик не станет равным 0.
package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    // Статическое задание количества задач ПЕРЕД запуском горутин
    wg.Add(2)

    go func() {
        defer wg.Done()
        fmt.Println("Горутина 1 завершила работу")
    }()

    go func() {
        defer wg.Done()
        fmt.Println("Горутина 2 завершила работу")
    }()

    wg.Wait() // Ожидание, пока счётчик не станет 0
    fmt.Println("Все горутины завершены")
}

Почему динамическое изменение (после вызова Wait) недопустимо?

  1. Нарушение контракта и гарантий безопасности: Вызов wg.Add() с положительным значением после начала выполнения wg.Wait() (или после того, как счётчик достиг нуля) нарушает логический контракт. Поведение становится неопределённым, и это может привести к панике (panic) или вечному ожиданию (deadlock). Согласно документации, Add должен быть вызван до Wait и обычно до запуска ожидаемой горутины.

    // НЕПРАВИЛЬНО! Может привести к панике.
    var wg sync.WaitGroup
    wg.Add(1)
    go func() { wg.Done() }()
    wg.Wait() // Счётчик становится 0
    
    // Попытка добавить задачу ПОСЛЕ ожидания - нарушение контракта.
    wg.Add(1) // ОПАСНО! Поведение не определено.
    
  2. Отсутствие необходимости в базовых сценариях: Основная цель WaitGroupстатическая синхронизация известного на момент запуска числа горутин или итераций цикла. Если количество задач неизвестно заранее, используются другие шаблоны.

Альтернативные паттерны для "динамических" сценариев

Если количество параллельных задач заранее неизвестно и может расти, следует применять иные подходы:

  1. Использование каналов (channels) для синхронизации:

    tasks := make(chan int)
    done := make(chan bool)
    
    // Worker-горутина
    go func() {
        for range tasks {
            // Обработка задачи
        }
        done <- true
    }()
    
    // Динамическое добавление задач
    for i := 0; i < dynamicCount; i++ {
        tasks <- i
    }
    close(tasks) // Сигнал завершения
    <-done       // Ожидание worker
    
  2. Комбинация WaitGroup с каналом или контекстом:

    var wg sync.WaitGroup
    stopChan := make(chan struct{})
    
    // Запуск динамического пула worker'ов
    for i := 0; i < initialPoolSize; i++ {
        wg.Add(1)
        go worker(i, stopChan, &wg)
    }
    
    // ... позже, возможно, запуск дополнительных worker'ов
    wg.Add(1) // Это ДОПУСТИМО, если Wait() ещё не был вызван!
    go worker(newID, stopChan, &wg)
    
    // Сигнал всем на завершение
    close(stopChan)
    wg.Wait() // Ожидаем всех, включая добавленных позже
    
  3. Использование errgroup.Group (пакет golang.org/x/sync/errgroup): Эта абстракция поверх WaitGroup позволяет динамически запускать задачи через Go() и легко обрабатывать ошибки.

    g, ctx := errgroup.WithContext(context.Background())
    
    // Задачи можно добавлять динамически
    g.Go(func() error { return doTask1(ctx) })
    g.Go(func() error { return doTask2(ctx) })
    
    // Ждём завершения всех, возвращаем первую ошибку
    if err := g.Wait(); err != nil {
        log.Fatal(err)
    }
    

Вывод

Стандартный sync.WaitGroup не поддерживает безопасное динамическое увеличение счётчика после того, как началось ожидание через Wait(). Его счётчик должен быть установлен (Add) до запуска операций, которые будут его уменьшать (Done), и до вызова Wait(). Если требуется более гибкое управление пулом динамически меняющихся горутин, следует использовать комбинации с каналами, контекстами или специализированные абстракции, такие как errgroup. Эти паттерны предоставляют безопасные и идиоматичные для Go способы решения задачи синхронизации при изменяющейся рабочей нагрузке.