Горутины и sync.WaitGroup: конкурентный запуск и ожидание
Содержание курса
sync.WaitGroup: Add до запуска, defer Done внутри, Wait в вызывающем коде
Чтобы main дождалась всех горутин, нужен механизм ожидания. sync.WaitGroup — самый прямолинейный: это счётчик, который вызывающий код инкрементирует перед запуском горутины и который горутина декрементирует при выходе.
package main
import (
"fmt"
"sync"
)
func greet(name string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("Hello,", name)
}
func main() {
var wg sync.WaitGroup
wg.Add(1)
go greet("Alice", &wg)
wg.Add(1)
go greet("Bob", &wg)
wg.Wait()
fmt.Println("all done")
}
Теперь all done появится только после того, как обе горутины отработали. Разберём три правила, которые здесь принципиальны.
wg.Add(1) — до go, не после. Если поставить Add после запуска горутины, планировщик вправе выполнить её целиком ещё до того, как Add зафиксирует единицу в счётчике. Тогда Done уменьшит счётчик ниже нуля — это паника sync: negative WaitGroup counter. Или, что хуже, Wait вернётся до завершения горутины, потому что в момент проверки счётчик уже равен нулю.
defer wg.Done() — первой строкой внутри горутины. defer гарантирует вызов Done при любом выходе из функции — в том числе при панике. Именно здесь пригождается механизм defer, с которым мы работали раньше: он не про «красивый код», а про надёжность освобождения ресурса.
wg.Wait() — после запуска всех горутин. Он блокирует текущую горутину до тех пор, пока счётчик не упадёт до нуля.
Один момент с передачей: WaitGroup нельзя копировать после первого использования — поэтому передаём по указателю (&wg) или используем замыкание. Значение, не указатель, приведёт к тому, что каждая горутина работает со своей копией счётчика и Wait не увидит их завершения.
