Comments 72
Количество операций в секунду — 6, количество потоков — 4, количество запросов — 50.6х4 = 20, должно выполниться за 2.5 секунды же? У вас вышло 5+.
А почему Вы спросили про приближенное? Вообще мне нужно ограничить количество определенных запросов от пользователя конкретной цифрой.
Когда приходит новый запрос в таймстемп t_current — увеличиваете счетчик на (t_current-t_last) * 10 и уменьшаете на 1. если получилось меньше 0 — значит количество запросов превышено. Если больше — то ок, сохраняете t_last = t_current.
И как t_current (t_current-t_last) * 10 может получиться меньше 0?
Не понимаю короче.
(t_current-t_last)*10 при разнице в полсекунды будет равно 5000.
Извините, может мне уже пора спать.
c_count = l_count + 1 — (c_time-l_time) / rps * 1000
Плюс данного метода в том, что нет пула, то есть нет расхода оперативной памяти и сложность инициализации О(1).
А вот минусы:
- (c_time-l_time) / rps * 1000 — теряет точность при rps кратному простому числу начиная с 3.
- Количество математических операций больше (то есть расчеты выполняются дольше чем в моем способе)
Так вот для моей задачи эти минусы критичны, а лишняя оперативка — нет.
Конечно же, формула будет такой
c_count = Math.Max(l_count + 1 — (c_time — l_time) /1000 * rps, 0)
Минуса с неточностью, следовательно, не будет. Но вот по производительности надо будет проверить. К ней требования выше, чем к жору оперативной памяти.
Проблема в том, что в ведро приходит целая операция, а уходит какая-то ее часть (эквивалент дельты времени).
В результате из ведра может утечь половина, но реально количество выполненных за последнюю секунду операций останется неизменным. В итоге функция пропускает операции, которые не должна была пропустить.
ЗЫ Я прежде чем чего то предлагать людям, стараюсь проверить сам. Попробуйте и вы. На данный момент способ видится мне нерабочим.
Есть коллекция отпечатков DateTime
А теперь подумайте, что случится при смене системного времени по тем или иным причинам. Для таких целей следует использовать монотонный таймер. В .NET таковым является Stopwatch
.
Достаточно хранить круговой массив из N дат последних запусков задач, если дата последнего больше периода — запускаете задачу, иначе ждете паузу в разницу между этим периодом и текущим временем.
Похоже на сценарий для НТ :)
А про таймер, тут вы не правы ИМХО. Таймер = дополнительная нагрузка.
В моем случае задача не стандартная и стандартных инструментов без потери в производительности для нее нет. В первую очередь код должен соответствовать своей задаче, не выполнять ничего лишнего (особенно там, где каждый тик на счету), и только во вторую должен быть понятным.
Поток будет спать не 99,9% времени в данном случае. Хотя это зависит от его периодичности. Тут во избежание ошибок периодичность должна быть 1 такт в миллисекунду. И то этого мало, это урезает точность выполнения, когда за 1 миллисекунду происходит больше одного запроса. И кроме всего прочего таймер (в .NET по крайней мере) гарантирует выполнение не чаще, чем установленная периодичность, а вот реже вполне может. Значит точность будет еще ниже.
Но как обстоят дела на Java, уважаемый Senior Java Developer, я не знаю. тут вам виднее.
В любом случае вы можете это проверить, запустив 100 потоков параллельно на цикл 1-100 с выводом счетчика и номера потока.
Но даже если таймер в яве работает точно как швейцарские часы, появляется просадка по точности. И точность эта 1мс. Хотя за 1 мс может быть выполнено довольно много запросов.
В моем коде на данный момент то же самое ограничение, но я легко могу его преодолеть, заменим штамп с ElapseMilliseconds на ElapsedTicks. Количество тиков за миллисекунду зависит от процессора и настроек системы. На моем домашнем (слабеньком) компе это значение равно около 300000. То есть точность будет максимальной (за 1 тик точно больше одной операции не будет выполнено).
Как то так.
А про AtomicInteger я не в курсе. В шарпе такого нет. Так что я не понимаю как это работает.
Я не знаю что делает BlockingQueue в Java, но в C# нет ничего, что мне бы подошло из коробки.
А про AtomicInteger я не в курсе. В шарпе такого нет.
Вам бы книжку какую почитать, а то получается что Вы не в курсе, а виноват почему-то C#
BlockingCollection Overview
Interlocked Operations
Проигрывает по скорости чего? Скорость выполнения CanExecute()?
Нужен критерий перформанса.
Через коллекцию токенов можно посмотреть как в Akka.Net реализован Throttle.
А через атомарные операции и таймер:
https://pastebin.com/fZ7EBGe3
Вызов таймера выполняется через ThreadPool
Тогда могу сказать что в Вашей задаче не хватает требований, которые Вы придумываете на лету, и которые никто кроме Вас не знает.
Способов реализации того, что описано в начале статьи (2 пункта) — масса. В том числе с помощью стандартных инструментов, с которыми Вы незнакомы.
Что в данном случае понимается под скоростью — остаётся загадкой.
Требования — она должна выполняться максимально быстро. Если в моем решении она выполняется быстрее, чем в вашем, вы проигрываете.
Я много с чем не знаком в платформе .NET. Как и вы, думаю. И если бы данная коллекция могла бы мне помочь, я бы с ней познакомился. Потому что я, прежде чем «изобретать велосипед», прочитал описание всех коллекций, которые смог найти. На некоторых останавливался чтобы изучить подробнее. И ничего мне не подошло.
Под скоростью в данном случае я имею ввиду время выполнения CanExecute()
Тогда спешу сообщить, что на моей машине вызов CanExecute() из моего решения с атомарными операциями занимает 12нс, а из вашего — 42нс.
Измерял BenchmarkDotNet.
Но я так понимаю в CanExecute() вы только добавляете слепок времени? А очистка происходит по таймеру?
Если что код я выложил: https://pastebin.com/fZ7EBGe3
В CanExecute() происходит атомарный декремент числа (никаких походов в heap, всё на стеке). Если число меньше 0, то атомарно инкрементим обратно.
По таймеру происходит обратное. Инкрементим, и если токенов стало очень много — декрементим.
Переменную можно сделать volatile чтобы она случайно незакешировалась в проце.
Ваша реализация ломает ограничение на количество операций в секунду.
Нет, не ломает. Желательно скриншот и объяснение.
Нет, не ломает. Желательно скриншот и объяснение.Фактически, то же самое предложил skyramp только без таймера. Математическая модель та же. Вместо таймера мы просто замеряем дельту времени между запросами при выполнении CanExecute(). То есть у вас происходят вычисления каждые Х мс, а без таймера просто при запросе эти вычисления умножатся на на количество мс между запросами.
Так вот такое решение на том же самом тесте, на котором я тестировал свое, пропустило 8 задач вместо 6. Все дело в том, что запрос при таком подходе оценивается как усредненное значение времени. Это будет работать только в том случае, если запросы будут поступать каждые s / rps секунд. То есть такой подход ломает ограничение. Более того, он позволит пользователям обойти его намеренно.
Экономия на выделении памяти под элементы — экономия на спичках.
Я не экономлю на памяти как раз.
Никто не запрещает перед основной работой сразу создать нужное количество элементов.Да, я именно так и делаю.
Кстати, не очень понятно, зачем в условии задачи привязка к количеству запусков в секунду. Очень похоже, что это переформулированная задача об одновременном существовании определенного количества объектов в нескольких потоках.Есть API, некоторые функции для каждого пользователя ограничены количеством выполнений в секунду. Ограничение это взято из статистики. В итоге сервер успевает обработать все входящие запросы и пользователи не ждут в очереди, пока отработают куча ненужных запросов других пользователей, которые, кстати, могут так наспамить, что мало не покажется. А так им придется организовывать работу под ограничение. Ну и плюс это какая никакая защита от ddos.
Для таких вещей придуманы семафоры. В частности в шарпе есть модификация SemaphoreSlim, которая позволяет задать сколько именно потоков будут иметь параллельный доступ к ресурсу. И нет, писать код за вас я не стану. На MSDN отличный пример для SemaphoreSlim
Вы мне рассказываете о чем моя задача? Зачем? Я не писал изначально о ее предназначении, потому что это не имеет значения.
Есть, BlockingCollection.
Товарищ omikron описал очень интересный алгоритм (который я возьму на вооружение). Раз в секунду в коллекцию кладётся нужное количество "разрешений". Например, всего 10 раз в секунду, метод в прошлую секунду выполнился 7 раз, значит в коллекции осталось 3 разрешения. Другой поток проходится по этой коллекции и "спит", если количество разрешений равно нулю. Особенность BlockingCollection в том, что цикл не заканчивается, если коллекция пустая.
Алгоритм очень лаконичный. Из математики там только одна операция вычитания.
Не всегда лаконичный код является наиболее верным. Далеко не всегда.
А выделять дополнительный поток для таймера — это вообще моветон! Я тут на копейках экономлю, а вы предлагаете косарик вложить :)
Между запросами в любом случае будет пауза. Хоть короткая, но будет.
По дороге с работы подумал над конкретными примерами применения обоих алгоритмов. Ваш стоит использовать для контроля количества запросов к API в мобильном приложении. Вариант с BlockingCollection я возьму для демона, который отправляет на вебхук агреггированные данные.
Собственно, да, разные цели и разные приоритеты — память или процессорное время. :)
и держать для этого отдельный поток с таймером?
//исходная последовательность событий
IObservable<MyEvent> source= ...;
//ограничения
var count = 5;
TimeSpan period = TimeSpan.FromSeconds(1);
//"обрезанная" последовательность
IObservable<MyEvent> cuttedStream = source
.WithLatestFrom(Observable.Interval(period).StartWith(-1), (src, timer) => new { src, timer })
.GroupBy(p => p.timer)
.SelectMany(p => p.Take(count))
.Select(p => p.src);
Ограничение количества выполнений метода в секунду