WaitGroup, Once, атомики

О лекции

WaitGroup ждёт набор горутин. Once гарантирует один вызов функции. atomic закрывает простые счётчики. Составное условие из нескольких полей атомиком не защитить.

Конкурентность Go golang собеседование

Помимо мьютекса и канала в пакете sync лежат простые инструменты. Без них легко написать гонку «запустил и сразу вышел». WaitGroup — счётчик незавершённых работ. Once — ленивая инициализация. atomic — инструкции процессора на одном слове, не транзакция над структурой.

На бэкенде WaitGroup собирает воркеров перед shutdown. Once поднимает клиент или регистрацию метрик. atomic крутит счётчики запросов.

Аналогия: WaitGroup — табличка «ещё N человек в смене». Once — «открыть магазин ровно один раз, даже если дёрнули ручку вдесятером». atomic — щелчок счётчика на турникете, не договор по двум полям сразу.

WaitGroup

Каждый Add(1) парный к Done. Done в defer, чтобы паника или ранний return всё равно уменьшили счётчик. Иначе Wait висит навсегда. Add должен произойти до того, как другая горутина может вызвать Wait и увидеть ноль. Обычно Add в той же горутине, что запускает работников, до go.

func runAll(tasks []Task) {
	var wg sync.WaitGroup
	for _, t := range tasks {
		wg.Add(1)
		go func(t Task) {
			defer wg.Done()
			t.Run()
		}(t)
	}
	wg.Wait()
}

Add до go. Done в defer. Задачу передают параметром, не через общую переменную цикла.

Пример. Написали go func() { defer wg.Done(); work() }(), а wg.Add(1) поставили внутрь горутины. Планировщик быстро переключил: Wait увидел ноль и вышел, работа ещё не началась.

Wait нельзя вызывать из тех же горутин, которые делают Done, если это создаёт цикл ожидания. Переиспользовать WaitGroup после Wait можно, если снова прийти к нулю. Классический баг цикла: все горутины видят последний t. С Go 1.22 у итерации своя переменная, но явный параметр по-прежнему яснее.

Once и atomic

sync.Once вызывает функцию один раз даже при гонке многих горутин. Паника внутри Once считается «вызов состоялся»; повторно не вызовут. Долгий RPC в Once держит всех ждущих. Для инициализации с I/O иногда лучше явный конструктор при старте процесса.

atomic.Int64 подходит для счётчиков без условия «два поля вместе». Проверка «если n == 0, то запиши флаг и n» из двух атомарных операций — уже гонка: между ними вклинится другая горутина. Тогда mutex.

Пример. Флаг ready и счётчик n. Два atomic.Store подряд не делают пару атомарной. Другая горутина может увидеть ready=true и старый n. Нужен один mutex вокруг обоих полей.

Типичная ошибка

Вызвать Wait, а Add сделать уже внутри горутины: при быстром планировщике Wait видит ноль и выходит. Забыть Done при ошибке. Заменить mutex на два atomic-поля и считать пару защищённой. Once с паникой — больше не повторится.

На собеседовании

Add до старта горутин, Done в defer, задачу в параметр замыкания. Once — один вызов, даже если внутри паника. atomic — простой счётчик. «Проверил и записал два поля» — mutex.