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

В чем разница между wg.Add в цикле и за циклом?

2.0 Middle🔥 191 комментариев
#Конкурентность и горутины

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

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

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

Разница между wg.Add в цикле и вне цикла

Разница в расположении вызова wg.Add() при использовании sync.WaitGroup напрямую влияет на логику синхронизации горутин и корректность выполнения программы. Этот вопрос проверяет понимание механики WaitGroup и её практического применения.

Основное назначение sync.WaitGroup

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

  • wg.Add(delta) увеличивает счетчик на delta (обычно на 1).
  • wg.Done() уменьшает счетчик на 1 (эквивалентно wg.Add(-1)).
  • wg.Wait() блокирует выполнение, пока счетчик не станет равен 0.

Вариант 1: wg.Add за циклом (вне цикла)

В этом подходе счетчик устанавливается один раз до запуска горутин, с указанием общего их количества.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    tasks := []string{"task1", "task2", "task3", "task4"}
    
    // Устанавливаем счетчик РАЗОМ на количество горутин
    wg.Add(len(tasks))
    
    for _, task := range tasks {
        go func(t string) {
            defer wg.Done() // Уменьшаем счетчик при выходе из горутины
            fmt.Println("Обрабатывается:", t)
        }(task)
    }
    
    wg.Wait() // Ожидаем завершения ВСЕХ горутин
    fmt.Println("Все задачи завершены")
}

Преимущества:

  • Атомарное установление счетчика: Нет гонки между wg.Add() и wg.Wait().
  • Проще для понимания: Четко видно, сколько горутин будет запущено.
  • Надежность: Исключается ситуация, когда wg.Wait() может быть вызван до wg.Add().

Недостатки:

  • Требует заранее знать точное количество горутин.
  • Не подходит для динамического запуска горутин внутри цикла (например, при условии).

Вариант 2: wg.Add в цикле

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

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    tasks := []string{"task1", "task2", "task3", "task4"}
    
    for _, task := range tasks {
        // Увеличиваем счетчик на каждой итерации
        wg.Add(1)
        go func(t string) {
            defer wg.Done()
            fmt.Println("Обрабатывается:", t)
        }(task)
    }
    
    wg.Wait()
    fmt.Println("Все задачи завершены")
}

Преимущества:

  • Гибкость: Можно запускать горутины условно внутри цикла.
  • Динамическое количество: Не нужно заранее знать точное число горутин.

Недостатки:

  • Потенциальная гонка: Если wg.Wait() вызван в другой горутине раньше, чем выполнились все wg.Add(), программа может завершиться преждевременно.
  • Сложнее для анализа: Требует внимания при рефакторинге.

Критически важные нюансы

Расположение wg.Add() относительно go:

// ОПАСНО: Гонка между добавлением и ожиданием
go func() {
    wg.Add(1) // В другой горутине - МОЖЕТ БЫТЬ ПОЗДНО
    defer wg.Done()
    // работа
}()

// ПРАВИЛЬНО: wg.Add() в той же горутине, где запуск
wg.Add(1)
go func() {
    defer wg.Done()
    // работа
}()

Динамические сценарии — когда wg.Add в цикле необходимо:

var wg sync.WaitGroup
for _, item := range items {
    if item.NeedsProcessing() { // Условие обработки
        wg.Add(1) // Точное количество неизвестно заранее
        go process(item, &wg)
    }
}

Лучшие практики

  1. Используйте defer wg.Done() — гарантирует уменьшение счетчика даже при панике.
  2. Передавайте WaitGroup по указателю — если передаете в функцию.
  3. Для фиксированного числа горутин — предпочтительнее wg.Add() за циклом.
  4. При динамическом запуске — используйте wg.Add() внутри цикла, но убедитесь, что нет гонок.
  5. Избегайте вызова wg.Add() внутри уже запущенной горутины — это антипаттерн.

Производительность

Разница в производительности минимальна, но wg.Add() за циклом предпочтительнее, так как:

  • Один вызов вместо N вызовов.
  • Меньше атомарных операций.
  • Нет накладных расходов на синхронизацию при инкрементации.

Вывод: Выбор между подходами зависит от контекста. Для известного количества горутин используйте wg.Add() за циклом — это безопаснее и читаемее. Для динамического/условного запуска — wg.Add() в цикле, но с осторожностью относительно потенциальных гонок. В любом случае, wg.Add() должен вызываться в той же горутине, которая порождает новые горутины, до вызова wg.Wait().

В чем разница между wg.Add в цикле и за циклом? | PrepBro