Как синхронизировать горутины при работе с каналами и воркер пулом в Go
Что проверяют
Умение собрать конвейер на каналах без дедлоков и утечек горутин. Интервьюер смотрит на четыре вещи: кто закрывает канал, где стоит WaitGroup, как возвращаются результаты и что происходит при отмене.
Как отвечать
Базовая схема: канал задач, N воркеров читают его в for range, WaitGroup ждёт их завершения, канал закрывает только отправитель.
func main() {
const workers = 3
jobs := make(chan int)
results := make(chan int)
var wg sync.WaitGroup
for w := 1; w <= workers; w++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := range jobs {
results <- j * 2
}
}(w)
}
go func() {
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
}()
go func() {
wg.Wait()
close(results)
}()
for r := range results {
fmt.Println(r)
}
}Правила, которые надо проговорить:
close(jobs)вызывает отправитель и ровно один раз. Закрытие из воркера или повторное закрытие это паника, отправка в закрытый канал тоже;for rangeпо каналу завершается сам при закрытии, отдельный стоп-сигнал не нужен;close(results)возможен только послеwg.Wait(), поэтому он вынесен в отдельную горутину. Если позватьwg.Wait()в main до чтения результатов, будет дедлок: воркеры заблокируются на записи, а никто не читает;wg.Add(1)вызывается доgo, а не внутри горутины, иначеWaitможет проскочить.
Отмена и ошибки. В воркер прокидывается context.Context, а запись результата делается через select:
select {
case <-ctx.Done():
return
case results <- v:
}Без этого воркер зависнет на записи в канал, который уже никто не читает, и горутина утечёт. Готовое решение под это: golang.org/x/sync/errgroup с g.SetLimit(n), он сам собирает первую ошибку и отменяет общий контекст.
Общее состояние между воркерами (счётчики, мапа результатов) защищается sync.Mutex или пакетом sync/atomic, наличие канала само по себе этого не делает. Проверять обязательно через go test -race.
Размер пула: для CPU-нагрузки ориентир runtime.NumCPU(), для IO больше, но конкретное число берут из замеров, а не из головы. Буферизация канала задач сглаживает всплески, но не заменяет ограничение параллелизма.
Где спотыкаются
Закрывают канал результатов в main сразу после отправки задач и ловят панику send on closed channel, либо ставят wg.Wait() перед чтением результатов и получают дедлок на первом же воркере.