Worker Pool в Go: зачем он вообще нужен?
В Go очень легко написать: for _, url := range urls { go fetch(url) } И это будет работать. Но представим, что у нас 10 000 задач. Получаем потенциально: 10 000 tasks ↓ 10 000 goroutines А если fetch() обращается к внешнему API, базе или файловой системе? Мы только что разрешили приложению одновременно запустить 10 000 операций. Иногда это совсем не то, что нам нужно. Worker Pool Идея простая: 10 000 jobs | v jobs channel | +--> worker 1 +--> worker 2 +--> worker 3 +--> ... +--> worker 10 Задач может быть 10 000, но одновременно выполняется только 10. Например: jobs := make(chan string)
for i := 0; i < 10; i++ { go worker(jobs) }
for _, url := range urls { jobs <- url }
close(jobs) Worker: func worker(jobs <-chan string) { for url := range jobs { fetch(url) } } Здесь происходит довольно простая вещь. jobs хранит очередь задач. Каждый worker ждёт задачу: for url := range jobs { fetch(url) } Как только worker закончил одну задачу, он берёт следующую. А как дождаться завершения всех workers? Для этого удобно использовать sync.WaitGroup: var wg sync.WaitGroup
for i := 0; i < 10; i++ { wg.Add(1)
go func() { defer wg.Done() worker(jobs) }() }
for _, url := range urls { jobs <- url }
close(jobs)
wg.Wait() Здесь важен порядок: создали workers ↓ отправили jobs ↓ закрыли jobs ↓ дождались workers После close(jobs) новые задачи отправлять нельзя. Но workers продолжат обрабатывать уже полученные задачи и завершатся, когда канал опустеет. Важный момент с WaitGroup Не стоит делать так: go func() { wg.Add(1) defer wg.Done()
worker(jobs) }() Add() должен выполняться до запуска goroutine. Иначе можно получить race между Add() и Wait(). Что мы получили? Без ограничения: 10 000 jobs ↓ 10 000 goroutines С Worker Pool: 10 000 jobs ↓ queue ↓ 10 workers То есть Worker Pool не делает goroutine "быстрее". Он решает другую задачу: контролирует количество одновременно выполняющейся работы. И это особенно полезно, когда работа упирается в ограниченный ресурс: внешний API; база данных; файловая система; CPU; сетевые соединения. Например, вместо: workers := 10_000 мы можем сознательно выбрать: workers := 20 И получить предсказуемый уровень concurrency. Но тут появляется следующий вопрос: а почему именно 20 workers? И вот здесь начинается уже самое интересное - размер Worker Pool зависит от того, чем именно занята worker goroutine: CPU, сетью, базой или чем-то ещё.