Конкурентность: горутины, каналы и контекст

Горутины и 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 не увидит их завершения.