Avatar ·

Как дождаться завершения всех горутин в Go без использования time.Sleep?

Всем привет! Я начал изучать Go и столкнулся с проблемой синхронизации горутин. Пишу простую программу, которая запускает несколько горутин для параллельной обработки данных, но никак не могу заставить главную функцию дождаться их завершения. Вот мой код: ```go package main import ( "fmt" "time" ) func worker(id int) { fmt.Printf("Worker %d started\n", id) time.Sleep(2 * time.Second) // имитация работы fmt.Printf("Worker %d finished\n", id) } func main() { for i := 1; i <= 5; i++ { go worker(i) } // Программа завершается сразу, не дожидаясь горутин fmt.Println("Main function ended") } ``` Проблема в том, что программа завершается мгновенно, не дожидаясь выполнения горутин. Я знаю, что можно использовать `time.Sleep()`, но это костыль — я не знаю точное время выполнения каждой горутины. К тому же, это ненадёжно и неэффективно. Как правильно синхронизировать горутины в Go? Нужно надежное решение без хардкода времени ожидания. Буду благодарен за примеры кода с пояснениями. <<>>

Использование sync.WaitGroup для ожидания горутин

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

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

Пример правильного кода

package main

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

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

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1) // Увеличиваем счётчик перед запуском горутины
        go worker(i, &wg)
    }

    wg.Wait() // Блокируем main до обнуления счётчика
    fmt.Println("All workers finished. Main function ended.")
}

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

  • wg.Add(n) — увеличивает внутренний счётчик на n. Вызывается до запуска горутины.
  • wg.Done() — уменьшает счётчик на 1. Обычно вызывается через defer в начале горутины.
  • wg.Wait() — блокирует выполнение до тех пор, пока счётчик не станет равным 0.
  • Передача по указателю — WaitGroup нужно передавать в горутины как *sync.WaitGroup, чтобы все горутины работали с одним экземпляром.

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

  • Вызов wg.Add() внутри горутины — это может привести к гонке данных. Всегда вызывайте wg.Add() до запуска горутины.
  • Забыть вызвать wg.Done() — приведёт к deadlock. Используйте defer wg.Done() сразу после объявления горутины.
  • Неверное количество вызовов wg.Add() — если добавить меньше, чем нужно, программа завершится раньше; если больше — будет deadlock.

Продвинутый пример с обработкой ошибок

package main

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

func worker(id int, wg *sync.WaitGroup, errCh chan<- error) {
    defer wg.Done()
    fmt.Printf("Worker %d started\n", id)
    time.Sleep(time.Duration(id) * time.Second)
    
    // Имитация ошибки у worker 3
    if id == 3 {
        errCh <- fmt.Errorf("worker %d failed", id)
        return
    }
    
    fmt.Printf("Worker %d finished\n", id)
}

func main() {
    var wg sync.WaitGroup
    errCh := make(chan error, 5)

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg, errCh)
    }

    wg.Wait()
    close(errCh)

    // Проверяем ошибки
    for err := range errCh {
        if err != nil {
            fmt.Println("Error:", err)
        }
    }
    fmt.Println("All workers finished.")
}

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

<<>>

Альтернативный подход: использование каналов

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

package main

import (
    "fmt"
    "time"
)

func worker(id int, done chan<- bool) {
    fmt.Printf("Worker %d started\n", id)
    time.Sleep(2 * time.Second)
    fmt.Printf("Worker %d finished\n", id)
    done <- true // Сигнал о завершении
}

func main() {
    done := make(chan bool, 5) // Буферизированный канал

    for i := 1; i <= 5; i++ {
        go worker(i, done)
    }

    // Ожидаем 5 сигналов завершения
    for i := 0; i < 5; i++ {
        <-done
    }
    fmt.Println("All workers finished. Main function ended.")
}

Когда выбирать каналы вместо WaitGroup

  • Если нужно собрать результаты работы горутин (например, строки или числа).
  • Если требуется обрабатывать завершение горутин в порядке их окончания.
  • Если нужно ограничить количество одновременно работающих горутин (worker pool).

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