Как дождаться завершения всех горутин в Go без использования time.Sleep?
Использование 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 является более идиоматичным и читаемым решением. Каналы лучше использовать, когда вам нужна двусторонняя связь между горутинами и главным потоком.
Ответов пока нет.
Войдите чтобы оставить ответ