Pull to refresh

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

Reading time 2 min
Views 8.6K
Пост про эрланг, но применим и ко всем другим языкам.

Пускай мы хотим раз в секунду что-то сделать, например проверить наличие файлика по 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 сообщений, то они все обработаются и после этого процесс быстро нормализуется и останется только одна волна перепосылки сообщений.
Tags:
Hubs:
+15
Comments 5
Comments Comments 5

Articles