Ответ
Существует два основных подхода: дождаться естественного завершения работы или принудительно сигнализировать об отмене.
1. Ожидание завершения с помощью sync.WaitGroup
Это идиоматичный способ дождаться, пока группа горутин завершит свою работу. WaitGroup
работает как потокобезопасный счетчик.
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
// Уменьшаем счетчик на 1, когда горутина завершается.
// defer гарантирует выполнение, даже если в функции произойдет паника.
defer wg.Done()
fmt.Printf("Рабочий %d началn", id)
time.Sleep(time.Second) // Симуляция работы
fmt.Printf("Рабочий %d закончилn", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
// Увеличиваем счетчик перед запуском каждой горутины
wg.Add(1)
go worker(i, &wg)
}
// Блокируем выполнение main, пока счетчик WaitGroup не станет равен 0
wg.Wait()
fmt.Println("Все рабочие завершили свою работу.")
}
Ключевые методы:
wg.Add(n)
: Увеличивает счетчик наn
.wg.Done()
: Уменьшает счетчик на 1.wg.Wait()
: Блокирует выполнение до тех пор, пока счетчик не станет равен нулю.
2. Сигнал к отмене с помощью context
Если нужно не просто дождаться, а иметь возможность принудительно остановить все горутины (например, по тайм-ауту или из-за ошибки), используется пакет context
.
// Концептуальный пример
ctx, cancel := context.WithCancel(context.Background())
// Запускаем горутины, передавая им ctx
for i := 0; i < 10; i++ {
go func(ctx context.Context) {
for {
select {
case <-ctx.Done(): // Если пришел сигнал отмены
fmt.Println("Горутина получила сигнал завершения")
return // Завершаем работу
default:
// ... выполняем полезную работу ...
}
}
}(ctx)
}
// Через какое-то время решаем остановить все горутины
time.Sleep(2 * time.Second)
cancel() // Эта функция отправит сигнал в <-ctx.Done() всем горутинам
Этот подход является предпочтительным для управления жизненным циклом горутин в современных Go-приложениях.