Периодическая посылка сообщений

в 1:12, , рубрики: erlang, Erlang/OTP, timer, утечки памяти, метки: , ,

Пост про эрланг, но применим и ко всем другим языкам.

Пускай мы хотим раз в секунду что-то сделать, например проверить наличие файлика по HTTP урлу. Процесс для этого мы стартовали, надо что бы он что-то делал достаточно регулярно.

В erlang есть три интересующих нас функции: timer:send_interval, timer:send_after и erlang:send_after.

Сначала объясню, почему нельзя пользоваться send_interval.

timer:send_interval проблемен тем, что он шлет сообщения не проверяя, обработано ли предыдущее. В итоге если выполнение задачи начинает залипать, то наш процесс только и занимается что этой задачей. В плохом случае начинается накапливание сообщений в очереди, утечка памяти и полная потеря отзывчивости процесса.

Я неоднократно наблюдал несколько сотен сообщений check в message_queue процесса.

Помимо этого начинается изнасилование внешнего ресурса: жесткого диска или удаленного сервера. Ведь вместо того, что бы делать перерыв в секунду, его может не быть вообще.

Итак, правильный шаг — перепосылать самому себе сообщение в конце обработки задачи.

Между timer:send_after и erlang:send_after выбор очевиден: erlang:send_after. Модуль timer делает это достаточно неоптимально и много таймеров начинают создавать проблемы.

Единственная причина не пользоваться erlang:send_after — это когда процессов много тысяч и можно группировать рассылку сообщений, не загружая систему непростыми в обработке таймерами, но это крайне редкая ситуация.

Однако тут легко допустить ошибку:

init([]) ->
  self() ! check,
  {ok, state}.

handle_info(check, State) ->
  do_job(State),
  erlang:send_after(1000, self(), check),
  {noreply, State}.

Что будет, если кто-то ещё пошлет в процесс сообщение check? Он породит вторую волну, ведь каждое сообщение check приводит к гарантированной перепостановке таймера.

В итоге, если послать 20 сообщений, то у процесса в каждый момент времени будет 20 таймеров, количество которых не уменьшится.

Правильный ответ такой:

init([]) ->
  Timer = erlang:send_after(1, self(), check),
  {ok, Timer}.

handle_info(check, OldTimer) ->
  erlang:cancel_timer(OldTimer),
  do_task(),
  Timer = erlang:send_after(1000, self(), check),
  {noreply, Timer}.


Явное снятие таймера приводит к тому, что если послать 1000 сообщений, то они все обработаются и после этого процесс быстро нормализуется и останется только одна волна перепосылки сообщений.

Автор: erlyvideo

Поделиться

* - обязательные к заполнению поля