Нужен пул воркеров, который сам подстраивает число горутин под нагрузку. Пока задач мало, работают minWorkers воркеров. Если в очереди копится бэклог, пул добавляет воркеров, но не больше maxWorkers. Когда работа кончилась, лишние воркеры по простою уходят сами, а minWorkers остаются.
type ScalingPool struct {
// ...
}
// NewScalingPool создаёт пул, который читает задачи из tasks.
func NewScalingPool(minWorkers, maxWorkers int, tasks <-chan func()) *ScalingPool
// Start запускает minWorkers воркеров и менеджер, который добавляет новых.
// Вызывается один раз.
func (p *ScalingPool) Start()
// Stop останавливает пул и возвращается, когда все его горутины завершились.
func (p *ScalingPool) Stop()
// Peak возвращает наибольшее число воркеров, существовавших одновременно.
func (p *ScalingPool) Peak() int
Что должно получиться:
- каждая задача, взятая из
tasks, выполняется ровно один раз;
- воркеров никогда не больше
maxWorkers и (пока пул работает) не меньше minWorkers;
- при бэклоге пул растёт выше
minWorkers, а Peak() это показывает;
- воркеры сверх
minWorkers, которым долго нечего делать, завершаются сами;
- после
Stop() не остаётся ни воркеров, ни горутины менеджера, а число горутин в процессе возвращается к исходному;
- нет гонок под
-race.
Пример. В буферизованный канал положили 200 задач по 5 мс каждая, запустили NewScalingPool(2, 16, tasks) и Start(). Сначала работают два воркера, бэклог большой, поэтому пул добавляет новых. Когда все 200 задач выполнены, Peak() больше 2 и не больше 16, а после Stop() горутин не осталось.
Тесты проверяют:
- каждая задача выполнена ровно один раз,
2 < Peak() <= 16 при бэклоге из 200 задач;
- границы по независимым измерениям, а не только по
Peak(): число одновременно выполняющихся задач и число горутин под нагрузкой не превышают maxWorkers, а при бэклоге пул вырастает выше minWorkers;
- уход по простою: после спада нагрузки число горутин возвращается к исходному плюс
minWorkers воркеров и менеджер (с запасом в одну-две горутины), на это отводится до 5 секунд;
- нижняя граница: в простое горутин не меньше
minWorkers, и пул после сжатия по-прежнему берёт новые задачи;
- после
Stop число горутин вернулось к исходному (с запасом в две).
Как работать с пулом: Stop прекращает приём задач и ждёт воркеров, которые уже выполняют функции. Задачи, оставшиеся в tasks, он не дочитывает. Если нужно выполнить всё, сначала дождитесь выполнения задач, затем вызовите Stop. Каждая задача должна когда-нибудь вернуться.