Как ограничить количество одновременно работающих горутин при большом количестве задач?
golangасинхронностьгорутинымногопоточностьограничение
Что проверяют
Понимаете ли вы, что горутины дёшевы, но внешние ресурсы нет, и знаете ли готовые способы ограничения.
Как отвечать
Запускать по горутине на задачу можно, но каждая пойдёт в базу или во внешний сервис, и упадёт уже он. Поэтому параллелизм ограничивают.
Первый способ, семафор на буферизованном канале:
sem := make(chan struct{}, 10) // не больше десяти одновременно
var wg sync.WaitGroup
for _, task := range tasks {
wg.Add(1)
go func() {
defer wg.Done()
sem <- struct{}{} // занять слот
defer func() { <-sem }() // освободить
process(task)
}()
}
wg.Wait()Второй, и в новом коде предпочтительный, это errgroup с ограничением:
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10)
for _, task := range tasks {
g.Go(func() error { return process(ctx, task) })
}
err := g.Wait() // первая ошибка, остальные отменены через контекстЧто стоит добавить:
- пул воркеров это третий вариант: заранее запускается N горутин, читающих задачи из канала. Он экономнее, когда задач миллионы, потому что горутины не создаются на каждую задачу;
- как выбрать число: для внешнего сервиса по его лимитам, для базы по размеру пула соединений, для вычислений по числу ядер. Цифра берётся из измерения, а не из головы;
- контекст обязателен: без него отменить пачку нельзя, и при уходе клиента работа продолжится впустую.
Где спотыкаются
Ставят семафор внутрь горутины после тяжёлой подготовки, и ограничение перестаёт работать.
Забывают освободить слот при ошибке, и пул постепенно исчерпывается.
Ограничивают число горутин, но каждая открывает своё соединение, и упирается не процессор, а база.