> Как реализовать worker pool для ограничения количества горутин в Go (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Ozon

Стек: Go

> Пример ответа

Короткий ответ

Worker pool - это паттерн для ограничения числа одновременно выполняющихся горутин. Основная идея: фиксированное количество воркеров читают задачи из канала и выполняют их. Реализуется через буферизированный канал задач, канал завершения (done) и sync.WaitGroup для ожидания завершения всех воркеров. Это позволяет контролировать потребление ресурсов, избегать перегрузки системы и обеспечивать предсказуемое поведение при высокой нагрузке.

Подробное объяснение

Worker pool решает две задачи: ограничение конкурентности и управление жизненным циклом горутин. Вместо того чтобы запускать горутину на каждую задачу (что может привести к исчерпанию памяти), мы создаём фиксированное число воркеров, которые обрабатывают задачи из общего канала.

Ключевые компоненты:

  • Канал задач - буферизированный или небуферизированный, через который передаются задания.
  • Канал завершения (done) - сигнализирует воркерам о необходимости завершить работу.
  • sync.WaitGroup - позволяет дождаться завершения всех воркеров перед выходом из функции.

Основной паттерн:

  1. Создаём канал задач и канал done.
  2. Запускаем N воркеров, каждый из которых в цикле слушает канал задач.
  3. Отправляем задачи в канал.
  4. Закрываем канал задач или отправляем сигнал в done, чтобы воркеры завершились.
  5. Ждём завершения всех воркеров через WaitGroup.

Важный нюанс: закрытие канала задач - это способ сообщить воркерам, что новых задач не будет. При этом воркеры должны корректно обрабатывать чтение из закрытого канала (получать zero value и завершаться).

На практике

При реализации worker pool важно учитывать:

  • Размер буфера канала задач - влияет на скорость приёма задач и потребление памяти. Если задачи поступают быстрее, чем обрабатываются, буфер заполнится, и отправитель заблокируется.
  • Graceful shutdown - нужно дать воркерам завершить текущую задачу перед остановкой.
  • Обработка ошибок - воркеры должны передавать ошибки в основной поток через отдельный канал или через структуру результата.
  • Контекст - для отмены работы по таймауту или внешнему сигналу лучше использовать context.Context.

Типичный сценарий: обработка HTTP-запросов, чтение из очереди сообщений, пакетная обработка данных. Worker pool позволяет ограничить количество одновременных операций ввода-вывода или CPU-интенсивных вычислений.

Пример кода

GO
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
// Имитация работы
time.Sleep(time.Millisecond * 100)
results <- job * 2
}
}
func main() {
const numJobs = 10
const numWorkers = 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// Запускаем воркеров
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// Отправляем задачи
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// Ждём завершения воркеров
wg.Wait()
close(results)
// Собираем результаты
for r := range results {
fmt.Println(r)
}
}

Более продвинутый вариант с context и graceful shutdown:

GO
func runWorkerPool(ctx context.Context, numWorkers int, tasks <-chan Task) <-chan Result {
results := make(chan Result)
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case task, ok := <-tasks:
if !ok {
return
}
results <- process(task)
}
}
}()
}
go func() {
wg.Wait()
close(results)
}()
return results
}

Как отвечать на собеседовании

Начните с определения паттерна и его назначения. Затем опишите структуру: канал задач, канал done, WaitGroup. Объясните, почему важно закрывать канал задач и как это взаимодействует с range по каналу. Упомяните trade-off между буферизированным и небуферизированным каналом. Покажите понимание graceful shutdown и обработки ошибок. Если спросят про альтернативы - упомяните errgroup из golang.org/x/sync для случаев, когда нужно остановить все воркеры при первой ошибке.

Что проверяет интервьюер

  • Понимание конкурентности в Go и работы с каналами.
  • Умение проектировать паттерны для ограничения ресурсов.
  • Знание sync.WaitGroup и корректного использования.
  • Понимание различий между закрытием канала и сигналом через done.
  • Способность рассуждать о edge cases: утечка горутин, deadlock, panic при отправке в закрытый канал.

Типичные ошибки

  • Deadlock - если воркеры ждут задачи, а отправитель ждёт, пока воркеры освободят буфер, и при этом буфер заполнен.
  • Утечка горутин - если не закрыть канал задач или не использовать done, воркеры будут висеть вечно.
  • Отправка в закрытый канал - panic. Нужно гарантировать, что отправка происходит до закрытия.
  • Игнорирование ошибок - воркеры должны передавать ошибки, а не молча их проглатывать.
  • Слишком большой буфер - может привести к неоправданному потреблению памяти.
  • Отсутствие WaitGroup - программа завершится, не дождавшись обработки всех задач.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью