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

Как дождаться выполнения нескольких горутин?

1.3 Junior🔥 211 комментариев
#Конкурентность и горутины

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

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

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

Как дождаться выполнения нескольких горутин в Go

В Go существует несколько основных подходов для ожидания завершения нескольких горутин. Каждый метод имеет свои особенности и применяется в разных сценариях.

1. Использование sync.WaitGroup

Самый распространенный и идиоматичный способ ожидания завершения группы горутин. WaitGroup предоставляет простой счетчик, который отслеживает количество запущенных и завершенных горутин.

package main

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

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

func main() {
    var wg sync.WaitGroup
    
    for i := 1; i <= 5; i++ {
        wg.Add(1) // Увеличиваем счетчик перед запуском горутины
        go worker(i, &wg)
    }
    
    wg.Wait() // Блокируемся, пока счетчик не станет равным 0
    fmt.Println("All workers completed")
}

Ключевые моменты:

  • Add(n) увеличивает счетчик на n единиц
  • Done() уменьшает счетчик на 1 (обычно вызывается через defer)
  • Wait() блокирует выполнение, пока счетчик не станет равным 0
  • WaitGroup должна передаваться по указателю

2. Использование каналов (Channels)

Каналы в Go могут использоваться для синхронизации горутин. Этот подход особенно полезен, когда нужно не только дождаться завершения, но и получить результаты работы.

package main

import (
    "fmt"
    "time"
)

func worker(id int, done chan<- bool) {
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
    done <- true // Отправляем сигнал о завершении
}

func main() {
    numWorkers := 5
    done := make(chan bool, numWorkers) // Буферизованный канал
    
    for i := 1; i <= numWorkers; i++ {
        go worker(i, done)
    }
    
    // Ждем завершения всех горутин
    for i := 0; i < numWorkers; i++ {
        <-done // Читаем из канала (блокирующая операция)
    }
    
    close(done)
    fmt.Println("All workers completed")
}

Преимущества подхода с каналами:

  • Возможность получать результаты работы
  • Более гибкая коммуникация между горутинами
  • Естественная интеграция с другими паттернами на каналах

3. select с каналами и таймаутами

Для ожидания с ограничением по времени можно использовать конструкцию select:

func waitWithTimeout(workers int, timeout time.Duration) {
    done := make(chan bool, workers)
    
    for i := 0; i < workers; i++ {
        go func(id int) {
            time.Sleep(time.Duration(id) * 500 * time.Millisecond)
            done <- true
        }(i)
    }
    
    for i := 0; i < workers; i++ {
        select {
        case <-done:
            fmt.Println("Worker completed")
        case <-time.After(timeout):
            fmt.Println("Timeout reached")
            return
        }
    }
}

4. Комбинирование подходов

На практике часто комбинируют несколько подходов для решения сложных задач:

package main

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

func worker(ctx context.Context, id int, wg *sync.WaitGroup, results chan<- int) {
    defer wg.Done()
    
    select {
    case <-time.After(time.Duration(id) * 500 * time.Millisecond):
        results <- id * 2
    case <-ctx.Done():
        fmt.Printf("Worker %d cancelled\n", id)
        return
    }
}

func main() {
    var wg sync.WaitGroup
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    
    results := make(chan int, 5)
    
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(ctx, i, &wg, results)
    }
    
    // Запускаем горутину для закрытия канала после завершения всех workers
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // Обрабатываем результаты
    for result := range results {
        fmt.Printf("Result: %d\n", result)
    }
    
    fmt.Println("All workers processed")
}

Сравнение подходов

МетодКогда использоватьПреимуществаНедостатки
WaitGroupПростое ожидание завершенияПростота, эффективностьНет возможности получения результатов
КаналыНужны результаты или более сложная синхронизацияГибкость, получение результатовБолее сложный код
select с таймаутамиОграничение времени выполненияКонтроль над временем выполненияДополнительная сложность
КомбинированныйСложные сценарииМаксимальная гибкостьНаибольшая сложность реализации

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

  1. Всегда используйте defer wg.Done() в начале функции worker для гарантированного уменьшения счетчика
  2. Вызывайте wg.Add() до запуска горутины, а не внутри нее
  3. Используйте буферизованные каналы при работе с результатами, чтобы избежать блокировок
  4. Рассмотрите использование context для отмены операций и таймаутов
  5. Избегайте одновременного вызова wg.Wait() и записи в каналы из одних и тех же горутин

Распространенные ошибки

// НЕПРАВИЛЬНО: передача WaitGroup по значению
func worker(wg sync.WaitGroup) { /* ... */ }

// ПРАВИЛЬНО: передача по указателю
func worker(wg *sync.WaitGroup) { /* ... */ }

// НЕПРАВИЛЬНО: вызов Add внутри горутины
go func() {
    wg.Add(1) // Может привести к гонкам
    // ...
}()

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

Выбор конкретного метода зависит от требований вашего приложения. Для простого ожидания завершения используйте sync.WaitGroup. Для получения результатов или более сложных сценариев синхронизации предпочтительнее каналы. В production-коде часто используют комбинацию context, WaitGroup и каналов для надежной и предсказуемой работы с горутинами.

Как дождаться выполнения нескольких горутин? | PrepBro