В основном он нужен для CPU heavy задач, где внутри горутины нет async io, то есть преимущества от большого кол-ва горутин нет, скорее только наоборот: шедулеру тяжелее, могут появлятся какие-то context switch'и с задачи на задачу (см. конец статьи)
Ну и так как все равно мы не можем выполнять больше NumCPU
таких задач одновременно, их можно поотбрасывать еще не начав считать, если, например, они уже протухли по таймауту.
Вопрос на засыпку гошникам: в чем разница между двумя имплементациями rate-limiter'а
(в будущем строчку
doStuff
я буду называть таской)
var rate = make(chan struct{},
runtime.NumCPU())
func HandleRequest(num int) {
wait := sync.WaitGroup{}
for i := 0; i < num; i++ {
wait.Add(1)
// THIS
rate <- struct{}{}
// THIS
go func() {
defer wait.Done()
defer func() {
<-rate
}()
// doStuff
}()
}
wait.Wait()
}
var rate = make(chan struct{},
runtime.NumCPU())
func HandleRequest(num int) {
wait := sync.WaitGroup{}
for i := 0; i < num; i++ {
wait.Add(1)
go func() {
defer wait.Done()
// OR THIS
rate <- struct{}{}
// OR THIS
defer func() {
<-rate
}()
// doStuff
}()
}
wait.Wait()
}
Для не-гошников, поясню, как оно работает: мы делаем канал (мультипоточную очередь) с буфером, размером в NumCPU (кол-во тредов). Это значит, что мы можем в него записать столько значений без блокировки, но следующее уже заблокируется, и будет ждать, пока из канала кто-то не прочитает. Соответственно, когда мы хотим заспавнить горутину/начать работать, мы пытаемся записать в очередь: если место есть (т.е. работает меньше NumCPU горутин), то мы запишем без блокировки и продолжим. А если места нет, то заблокируемся, и будем ждать, когда место появится. Соответственно, остаётся только читать значение обратно, когда мы завершаем работу, таким образом освобождая место в канале.
Очевидно, что в первом случае мы не будем спавнить горутину, что имеет свой плюс и свой минус:
Но это не единственная разница!
В Go каналы FIFO, в том числе на блокировках (т.е. тот, кто первый встал в очередь на чтение/запись, тот и будет в итоге первым)
Представим ситуацию, в которой к нам резко пришло много запросов
К сожалению, в моем случае это скорее норма, чем исключение, так как frontend может послать нам 3-5+ запросов одновременно, когда юзер открывает страницу.
Давайте для простоты считать, что NumCPU=10, а к нам пришло 15 запросов каждый с 20-ю тасками, и рейтлимитер сейчас заполнен какими-то старыми тасками (но очередь на него пустая).
Что произойдет в каждом из случаев?
Каждый запрос будет просить рейтлимитер, но только в одной горутине (запроса) каждый. При этом, запросов больше, чем буфер рейтлимитера, поэтому часть заблокируется и встанет в очередь. После того, как какой-то запрос заспавнит себе горутину, он опять встанет в очередь рейт лимитера, причем в ее конец.
Или, если кратко, то тут выстраивается очередь из запросов
Допустим, что между запросами есть какая-то минимальная задержка (достаточная, чтобы просто заспавнить 20 горутин) - опять же, для простоты (в реальности какая-то задержка тоже будет) Тогда, к нам придет первый запрос, и забьет своими тасками. Потом придет следующий запрос, и заполнит своими тасками.
Или, если кратко, то тут выстраивается очередь из тасок