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

Для чего нужен Wg.Wait?

1.2 Junior🔥 72 комментариев
#Основы Go

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

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

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

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

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

Ключевая механика работы

WaitGroup работает по принципу счетчика с тремя основными методами:

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

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

func main() {
    var wg sync.WaitGroup
    
    for i := 1; i <= 3; i++ {
        wg.Add(1) // Увеличиваем счетчик перед запуском каждой горутины
        go worker(i, &wg)
    }
    
    fmt.Println("Главная горутина: запустила воркеров и ждёт...")
    wg.Wait() // Блокировка здесь до завершения всех worker()
    fmt.Println("Главная горутина: все воркеры завершились, программа завершает работу.")
}

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Уменьшаем счетчик при выходе из функции (гарантированно)
    fmt.Printf("Воркер %d: начал работу\n", id)
    time.Sleep(time.Duration(id) * time.Second) // Имитация работы
    fmt.Printf("Воркер %d: завершил работу\n", id)
}

Типичные сценарии использования

  1. Ожидание завершения группы параллельных задач

    // Параллельная обработка элементов слайса
    results := make([]string, len(items))
    var wg sync.WaitGroup
    
    for i, item := range items {
        wg.Add(1)
        go func(idx int, data Item) {
            defer wg.Done()
            results[idx] = process(data) // Конкурентная обработка
        }(i, item)
    }
    
    wg.Wait() // Ждем обработки всех элементов
    // Теперь results полностью заполнен
    
  2. Оркестрация этапов конвейерной обработки (pipeline)

    // Этап 1: читаем данные
    stage1 := make(chan Data)
    go producer(stage1)
    
    // Этап 2: обрабатываем данные несколькими воркерами
    stage2 := make(chan Result)
    var wg sync.WaitGroup
    
    for i := 0; i < workerCount; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for data := range stage1 {
                stage2 <- process(data)
            }
        }()
    }
    
    // Отдельная горутина для закрытия stage2 после завершения всех воркеров
    go func() {
        wg.Wait()    // Ждем завершения всех воркеров
        close(stage2) // Закрываем канал выхода
    }()
    
  3. Ограничение времени выполнения группы операций

    var wg sync.WaitGroup
    done := make(chan struct{})
    
    for _, task := range tasks {
        wg.Add(1)
        go func(t Task) {
            defer wg.Done()
            t.Execute()
        }(task)
    }
    
    // Горутина, которая сигнализирует о завершении всех задач
    go func() {
        wg.Wait()
        close(done)
    }()
    
    // Ожидаем либо завершения всех задач, либо таймаута
    select {
    case <-done:
        fmt.Println("Все задачи завершены успешно")
    case <-time.After(5 * time.Second):
        fmt.Println("Таймаут: не все задачи завершились за отведенное время")
    }
    

Важные аспекты и лучшие практики

  • Передача по указателю: WaitGroup должен передаваться в функции по указателю, так как содержит внутреннее состояние, которое должно изменяться.
  • Правильный порядок вызовов: Add() следует вызывать до запуска горутины, а не внутри неё, чтобы избежать гонок.
  • Гарантированное уменьшение счетчика: Используйте defer wg.Done() для гарантированного уменьшения счетчика даже при панике.
  • Отрицательный счетчик: Вызов Done() больше раз, чем Add(), приведет к панике с отрицательным счетчиком.
  • Повторное использование: После того как Wait() вернул управление, WaitGroup можно использовать снова, но нельзя начинать новое ожидание, пока предыдущее еще не завершилось.

Сравнение с альтернативами

  • Каналы: Можно использовать каналы для синхронизации, но WaitGroup более идиоматичен и эффективен для сценариев "ожидания N операций".
  • errgroup.Group: Пакет golang.org/x/sync/errgroup расширяет WaitGroup, добавляя обработку ошибок и контекст для отмены.
  • sync.Once: Для однократного выполнения, а не ожидания группы операций.

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

Для чего нужен Wg.Wait? | PrepBro