Таймер — механизм, позволяющий в заданный момент времени запустить какое-то событие.
В Эрланге самым простым примером использования таймера является синхронная функция timer:sleep(Time), заставляющая текущий актор замереть на Time миллисекунд и ничего не делать. Когда наступит заданный момент времени, актор возобновит свою работу.
В качестве таймера можно использовать оператор receive ... end, позволяющий использовать ключевое слово after с указанием периода — количества миллисекунд. Если за это время оператор не получит новое сообщение, сработает соответствующая кляуза. В следующем примере циклическая функция будет раз в 5 секунд печатать слово “hello”.
loop() ->
io:format("hello~n"),
receive
% тут может быть обработка входящих сообщений
after 5_000 -> loop()
end.
Однако, если нам важно, чтобы функция одновременно и обрабатывала входящие сообщения (коих может оказаться много), и делала что-то циклически, тут очень быстро столкнёмся с проблемой. Когда сообщения будут приходить ежесекундно, таймер перестанет срабатывать. Всё-таки функция должна быть предназначена для решения какой-то одной главной задачи.
Поэтому планирование событий во времени будет правильным решать с помощью стороннего актора. Его легко можно сделать самому с помощью того же самого оператора receive ... end, но только без обработки входящих сообщений:
loop() ->
% Проверка условий
% и запуск определённых событий,
% когда то или иное условие удовлетворено.
receive
after 1_000 -> loop()
end.
Если мы желаем понизить точность (чтобы проверка условий происходила раз в минуту, а не секунду), можно указать after 60_000. Если наоборот требуется повысить точность, можно уменьшить период. При минимально возможном периоде — 1 — проверка будет осуществляться раз в 1 миллисекунду.
Однако такой подход не очень гибок, потому что актор становится глухим и ни на что не реагирует. Если желаем добавить новой периодики, придётся переписывать программу. А принимать сообщения для таймера, как мы уже выяснили, довольно опасно. Поэтому в большинстве случаев проще воспользоваться встроенным в Эрланг таймером:
Например, с помощью erlang:start_timer/3 мы запускаем таймер, указывая в качестве аргументов временной период, адресат и сообщение, которое будет послано адресату. Через указанное время актор-адресат получит сообщение.
1> erlang:start_timer(5_000, self(), "privet").
#Ref<0.3761145888.3162767364.25978>
2> flush().
Shell got {timeout,#Ref<0.3761145888.3162767364.25978>,"privet"}
Таймер можно использовать многократно, задавая ему параллельно много задач. Функции start_timer и send_after возвращают уникальный идентификатор задачи таймера. start_timer, в отличие от собрата, потом этот идентификатор пошлёт адресату. Это удобно для менеджмента задач таймера.
С помощью erlang:read_timer/1-2 можно узнать текущее состояние таймера, т.е. сколько миллисекунд осталось до его срабатывания. Таймер из модуля timer не позволяет узнать своё состояние, зато с помощью него можно задавать циклические события, что часто весьма актуально.
Данные таймеры опираются на монотонное время. Это в целом правильно, однако может возникнуть такой вопрос. А что делать, если нам нужно запустить какое-то событие по системному времени, причём с высокой точностью? Например, мы не желаем провести на рабочем месте даже лишней миллисекунды, но и раньше времени собираться домой нам начальство не позволяет, поэтому хотим, чтобы в будние дни ровно в 18:00 плюс-минус 1 миллисекунда поступал сигнал об окончании рабочего дня.
Эту задачу стоит решать с помощью менеджера задач таймера. Делается это так. Менеджер узнаёт от нас, что мы желаем ровно в 18:00 получить сигнал. С помощью erlang:time_offset он узнаёт текущую разницу между монотонным и системным временем, вычисляет абсолютное значение монотонного времени, когда должен сработать таймер. Устанавливает таймер на это абсолютное время, используя опцию {abs,true}.
Какое-то время не будет вообще никакого сдвига монотонного времени относительно системного. Но рано или поздно случится неминуемое — системные часы будут грубо переведены вперёд или назад. Как наш менеджер должен это учесть? Во время срабатывания таймера? Но тогда может быть уже поздно (так будет в 50% случаев). Выход один — менеджер должен сразу же узнать, что системные часы были переведены, поэтому возник новый сдвиг. Для этого актор-менеджер должен быть подписан на события такого рода — с помощью erlang:monitor(time_offset, clock_service). При каждом изменении сдвига он будет получать сообщения.
Получив такое сообщение, менеджер должен отменить ту часть задач таймера, привязанных к системному времени. Для каждой такой задачи он вычисляет новое абсолютное значение монотонного времени, когда должен сработать таймер, и ставит таймеру новые задачи с помощью erlang:start_timer.
В целом менеджер задач таймера призван делать следующее:
Если мы уважаем свою Систему и задаёмся целью полностью восстановить её былое состояние после перезапуска, надо озаботиться и сохранением состояния таймера. Иначе может быть нарушена какая-то важная логика. Например, Система должна раз в месяц (но не чаще) делать что-то важное, допустим, посылать отчёт нашему начальству. Если мы раз в две недели вынуждены перезагружать сервер и при этом не сохраняем состояние таймера, он заново начнёт отсчёт месяца, и наш начальник никогда отчёта не получит.
Тут возможен и другой нюанс. Нам, вероятно, придёт в голову, что просто надо ежемесячно в один и тот же день, один и тот же час генерировать нужное событие (то есть ориентироваться на абсолютное системное время). Однако нужный день и час может выпасть как раз на тот момент, когда наши компьютеры не работают. Система запустится — и события (например, отправки отчёта) не случится, потому что нужный час уже прошёл, надо ждать следующего месяца.
Такого рода проблемы тоже должен решать менеджер задач таймера. В одних случаях событие должно быть запущено сразу после перезагрузки. Лучше поздно, чем никогда — это уместно для примера с отчётом. В других случаях можно и нужно проигнорировать запуск события — ведь время уже прошло. Например, наш компьютер может ежечасно оповещать нас курантами о наступлении нового часа. Было бы неправильно делать это в неположенное время (не в 15:00, а 15:07).
Со временем всегда надо работать аккуратно.
Очевидно, если у таймера будет слишком много параллельных задач, это приведёт к нежелательным результатам: будет повышенная нагрузка на процессор, события перестанут запускаться с точностью до миллисекунды (как обещано в документации). Но где же предел? Мои эксперименты показали следующее:
Copyright © 2026 Алексей Карманов