sync.WaitGroup
В прошлом уроке main закрывала кухню, не дожидаясь поваров, и их работа пропадала. Настоящий шеф так не делает. У него на стене висит доска с числом заказов, которые ещё готовятся. Принял заказ — прибавил единицу, повар отдал блюдо — вычел. Кухня закрывается, только когда на доске ноль.
Счётчик горутин
В Go такая доска — тип sync.WaitGroup из пакета sync. Это счётчик горутин, которые ещё не закончили работу:
package main
import "fmt"
import "sync"
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("hello") // hello
}()
wg.Wait()
fmt.Println("main") // main
}Давайте разберём пример по шагам:
wg.Add(1)записывает на доску один заказ: счётчик становится1.go func() { … }()запускает горутину, и она печатаетhello.- На выходе из горутины
defer wg.Done()вычитает единицу. - Тем временем
wg.Wait()вmainждёт, пока счётчик не дойдёт до нуля. - Только после этого
mainпечатаетmain.
Поэтому программа печатает обе строки при каждом запуске, и hello всегда идёт первой.
Программа импортирует пакеты двумя строками import: это то же самое, что одна строка со скобками.
Add, Done и Wait
У WaitGroup три метода. Нулевое значение WaitGroup готово к работе: счётчик в нём равен нулю.
Add(n)
Метод Add(n) увеличивает счётчик на n.
Этот вопрос часто задают на собеседовании: где вызывать Add? Ответ: до оператора go, в той горутине, которая потом вызовет Wait. Если перенести Add внутрь новой горутины, Wait может выполниться раньше него, увидеть ноль и вернуться, пока работа не сделана. Шеф закроет кухню, потому что заказ ещё не успели записать на доску.
Done()
Метод Done() уменьшает счётчик на единицу. Его вызывают через defer первой строкой горутины, чтобы он сработал при любом выходе из функции.
Если вызвать Done больше раз, чем прибавил Add, случится паника sync: negative WaitGroup counter. Отрицательного числа заказов не бывает.
Wait()
Метод Wait() ждёт, пока счётчик не станет равен нулю.
Здесь аналогия с доской заканчивается: на доске шефа можно написать, кто что готовит, а WaitGroup хранит одно число. Он не знает, какие горутины ещё работают, и не подскажет, кого ждёт Wait.
Результаты в общем срезе
Горутины часто пишут результаты в общий срез, каждая в свой элемент. Тогда результат не зависит от того, в каком порядке они работали:
var wg sync.WaitGroup
squares := make([]int, 3)
for i := range 3 {
wg.Add(1)
go func() {
defer wg.Done()
squares[i] = i * i
}()
}
wg.Wait()
fmt.Println(squares) // [0 1 4]Разберём, почему результат всегда один:
- каждая горутина кладёт своё блюдо на свою полку:
squares[0],squares[1]илиsquares[2]; - две горутины никогда не пишут в один элемент, поэтому порядок их работы не важен;
- после
Waitвсе три полки заполнены, и программа печатает[0 1 4].
Метод Go
Начиная с Go 1.25 у WaitGroup есть метод Go. Вызов wg.Go(f) делает три шага:
- Увеличивает счётчик на единицу.
- Запускает
fв новой горутине. - Вызывает
Done, когдаfвернётся.
Цикл выше становится короче:
var wg sync.WaitGroup
squares := make([]int, 3)
for i := range 3 {
wg.Go(func() {
squares[i] = i * i
})
}
wg.Wait()
fmt.Println(squares) // [0 1 4]Перепутать порядок Add и go здесь нельзя: метод делает и то и другое сам.
Частая ошибка
WaitGroup передали в функцию по значению:
func work(wg sync.WaitGroup) { // ✗
defer wg.Done()
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
go work(wg)
wg.Wait()
}fatal error: all goroutines are asleep - deadlock!Счётчик в main так и не доходит до нуля:
- Функция получила копию доски.
workуменьшает счётчик своей копии, а счётчик вmainостаётся равным единице.Waitждёт вечно, и, когда заблокированы все горутины, среда выполнения останавливает программу.
WaitGroup никогда не копируют: функция принимает wg *sync.WaitGroup, а вызов передаёт &wg.
Главное
WaitGroup— счётчик незаконченных горутин:Add(n)прибавляет,Done()вычитает единицу,Wait()ждёт нуля.Addвызывают до оператораgo;wg.Go(f)делаетAdd,goиDoneсам.Doneчаще, чемAdd, вызывает паникуsync: negative WaitGroup counter.WaitGroupне копируют: в функцию передают указатель&wg.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.